SQLDbWrpr 5.1.0__tar.gz → 5.2.0__tar.gz
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.
- {sqldbwrpr-5.1.0 → sqldbwrpr-5.2.0}/PKG-INFO +122 -2
- {sqldbwrpr-5.1.0 → sqldbwrpr-5.2.0}/README.md +121 -1
- {sqldbwrpr-5.1.0 → sqldbwrpr-5.2.0}/pyproject.toml +1 -1
- {sqldbwrpr-5.1.0 → sqldbwrpr-5.2.0}/src/sqldbwrpr/sqldbwrpr.py +253 -87
- {sqldbwrpr-5.1.0 → sqldbwrpr-5.2.0}/LICENSE.txt +0 -0
- {sqldbwrpr-5.1.0 → sqldbwrpr-5.2.0}/src/sqldbwrpr/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SQLDbWrpr
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.2.0
|
|
4
4
|
Summary: Wrapper for importing CSV and Text files into MySQL and Postgress
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE.txt
|
|
@@ -65,7 +65,7 @@ When both are supplied, `p_db_structure` takes precedence for backward compatibi
|
|
|
65
65
|
- Import CSV data from files or in-memory rows, including single-volume and numbered multi-volume files.
|
|
66
66
|
- Export full tables or custom SQL query results to CSV, including optional multi-volume exports.
|
|
67
67
|
- **Database Support**: Includes MySQL and PostgreSQL wrappers with dialect-specific SQL rendering.
|
|
68
|
-
- **User and Permission Management**: Create MySQL users and grant database rights.
|
|
68
|
+
- **User and Permission Management**: Create and delete MySQL users or PostgreSQL login roles, and grant backend-specific database or table rights.
|
|
69
69
|
- **Batch Processing**: Configure import batch sizes for larger CSV loads.
|
|
70
70
|
|
|
71
71
|
### Project Structure
|
|
@@ -83,6 +83,17 @@ When both are supplied, `p_db_structure` takes precedence for backward compatibi
|
|
|
83
83
|
pip install SqlDbWrpr
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
+
### Running the Tests
|
|
87
|
+
|
|
88
|
+
The focused database tests use temporary MySQL 8.0 and PostgreSQL 16 Docker containers. Ensure Docker is running, then install the development dependencies and run the test suite:
|
|
89
|
+
|
|
90
|
+
```powershell
|
|
91
|
+
poetry install --with dev
|
|
92
|
+
poetry run pytest
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The test fixtures wait for each database to become ready and remove the temporary containers after the test session.
|
|
96
|
+
|
|
86
97
|
### Quick Start With A Legacy Structure
|
|
87
98
|
|
|
88
99
|
```python
|
|
@@ -167,6 +178,115 @@ db = PostgreSQL(
|
|
|
167
178
|
db.import_csv("Users", p_csv_db=[("Username",), ("alice",)], p_vol_type="Single")
|
|
168
179
|
```
|
|
169
180
|
|
|
181
|
+
## Function Reference
|
|
182
|
+
|
|
183
|
+
This section documents the callable API in `src/sqldbwrpr/sqldbwrpr.py`.
|
|
184
|
+
|
|
185
|
+
### `SchemaSourceError`
|
|
186
|
+
|
|
187
|
+
- `SchemaSourceError(ValueError)`: Raised when no usable schema source is provided to initialize a wrapper (`p_db_structure`, `p_sqlalchemy_metadata`, or `p_sqlalchemy_base.metadata`).
|
|
188
|
+
|
|
189
|
+
### `SQLDbWrpr` (base wrapper)
|
|
190
|
+
|
|
191
|
+
- `__init__(p_host_name, p_user_name, p_password, p_recreate_db, p_db_name, p_db_structure, p_sqlalchemy_base, p_sqlalchemy_metadata, p_batch_size, p_bar_len, p_msg_width, p_verbose, p_db_port, p_ssl_ca, p_ssl_key, p_ssl_cert)`
|
|
192
|
+
- Initializes shared wrapper state, resolves schema source, configures import/export behavior, and precomputes char vs non-char field maps.
|
|
193
|
+
- Schema resolution order: explicit `p_db_structure` first, then SQLAlchemy metadata, else raises `SchemaSourceError`.
|
|
194
|
+
- `close()`
|
|
195
|
+
- Closes the active DB connection if present.
|
|
196
|
+
- `build_column_sql(p_field_name, p_field_type, p_field_params, p_field_comment)`
|
|
197
|
+
- Builds the column fragment for `CREATE TABLE` in the active dialect (base implementation is MySQL-oriented).
|
|
198
|
+
- Applies `AUTO_INCREMENT`, `UNSIGNED`, `NOT NULL`, `ZEROFILL`, defaults, and comments based on field parameters.
|
|
199
|
+
- `build_index_sql(p_table_name, p_idx_name, p_idx_fields, p_unique=False)`
|
|
200
|
+
- Builds an index clause with sort directions from legacy index metadata.
|
|
201
|
+
- `build_insert_sql(p_table_name, p_header, p_replace=False)`
|
|
202
|
+
- Builds an `INSERT` or `REPLACE` statement with backend parameter placeholders.
|
|
203
|
+
- `param_placeholder()`
|
|
204
|
+
- Returns the DB-API parameter token used by execution (`%s`).
|
|
205
|
+
- `quote_identifier(p_identifier)`
|
|
206
|
+
- Quotes SQL identifiers when a backend quote character is configured; otherwise returns plain identifier text.
|
|
207
|
+
- `quote_identifier_list(p_identifiers)`
|
|
208
|
+
- Quotes and joins a list of identifiers using commas.
|
|
209
|
+
- `render_default_sql(p_field_type, p_default_value)`
|
|
210
|
+
- Renders a `DEFAULT` clause, quoting string-like defaults for char/varchar types.
|
|
211
|
+
- `render_field_type(p_field_type, p_field_params)`
|
|
212
|
+
- Renders legacy type arrays into SQL type declarations (e.g., `varchar(n)`, `decimal(p,s)`).
|
|
213
|
+
- `create_db()`
|
|
214
|
+
- Base database recreation flow: drops an existing DB and creates/selects a fresh one.
|
|
215
|
+
- `create_tables()`
|
|
216
|
+
- Validates legacy schema metadata and creates tables, keys, indexes, and constraints in dependency order.
|
|
217
|
+
- Splits table creation and post-create operations where backend rules require deferred statements.
|
|
218
|
+
- `create_users(p_admin_user, p_new_users)`
|
|
219
|
+
- Creates MySQL users if they do not already exist.
|
|
220
|
+
- `delete_users(p_admin_user, p_del_users)`
|
|
221
|
+
- Drops MySQL users that exist in `mysql.user`.
|
|
222
|
+
- `_err_broken_rec(p_sql_str, p_csv_db_slice)`
|
|
223
|
+
- Retry helper used during imports to isolate/log failing rows and stop on first unrecoverable record.
|
|
224
|
+
- `export_to_csv(p_csv_path, p_table_name, p_delimiter="|", p_strip_chars="", p__vol_size=0, p_sql_query="")`
|
|
225
|
+
- Exports table/query results to CSV in single-file or multi-volume mode.
|
|
226
|
+
- Multi-volume mode chunks output by record count and generates numbered files.
|
|
227
|
+
- `get_db_field_types()`
|
|
228
|
+
- Populates per-table lists of character vs non-character columns for type-sensitive import handling.
|
|
229
|
+
- `grant_rights(p_admin_user, p_user_rights)`
|
|
230
|
+
- Grants MySQL rights and corresponding grant-option privileges according to supplied rights tuples.
|
|
231
|
+
- `import_csv(p_table_name, p_csv_file_name="", p_key="", p_header="", p_del_head=False, p_csv_db="", p_csv_corr_str_file_name="", p_vol_type="Multi", p_verbose=False, p_replace=False)`
|
|
232
|
+
- Imports CSV data from file(s) or in-memory rows.
|
|
233
|
+
- Supports single- and multi-volume input, optional header override/removal, correction-string preprocessing, type conversion, date normalization, and batched insert/replace.
|
|
234
|
+
- `import_and_split_csv(p_split_struct, p_data, p_header="", p_insert_header=False, p_verbose=False, p_debug=False)`
|
|
235
|
+
- Splits an input dataset into multiple destination table payloads using declarative field mappings and transform commands, then imports each generated dataset.
|
|
236
|
+
- `from_sqlalchemy_metadata(p_sqlalchemy_metadata)` (`@classmethod`)
|
|
237
|
+
- Converts SQLAlchemy `MetaData` (sorted tables) to the legacy `db_structure` dictionary.
|
|
238
|
+
- `resolve_db_structure(p_db_structure=None, p_sqlalchemy_base=None, p_sqlalchemy_metadata=None)` (`@staticmethod`)
|
|
239
|
+
- Central schema source resolver used by constructors; raises `SchemaSourceError` when none are provided.
|
|
240
|
+
- `_action_to_legacy_code(p_action)` (`@staticmethod`)
|
|
241
|
+
- Maps SQLAlchemy FK action text (e.g., `CASCADE`, `SET NULL`) to legacy action codes (`C`, `N`, etc.).
|
|
242
|
+
- `_build_default_field_params()` (`@staticmethod`)
|
|
243
|
+
- Produces the baseline legacy field-parameter block used during SQLAlchemy conversion.
|
|
244
|
+
- `_column_type_to_legacy(p_column)` (`@staticmethod`)
|
|
245
|
+
- Converts a SQLAlchemy column type object into the legacy type-array format.
|
|
246
|
+
- `_column_to_legacy_field(p_column)` (`@staticmethod`)
|
|
247
|
+
- Converts one SQLAlchemy column into a legacy field definition, including nullability, autoincrement, defaults, and comments.
|
|
248
|
+
- `_set_foreign_keys(p_table, p_table_structure)` (`@staticmethod`)
|
|
249
|
+
- Writes legacy foreign-key metadata onto converted table fields.
|
|
250
|
+
- `_set_indexes(p_table, p_table_structure)` (`@staticmethod`)
|
|
251
|
+
- Writes legacy index metadata for converted table fields.
|
|
252
|
+
- `_set_primary_key(p_table, p_table_structure)` (`@staticmethod`)
|
|
253
|
+
- Marks primary-key columns in converted legacy table definitions.
|
|
254
|
+
- `_table_to_legacy_structure(p_table)` (`@staticmethod`)
|
|
255
|
+
- Converts an entire SQLAlchemy table into a legacy table structure and enriches it with keys/indexes.
|
|
256
|
+
- `_print_err_msg(p_err, p_msg="")` (`@staticmethod`)
|
|
257
|
+
- Formats and prints database error details before termination paths.
|
|
258
|
+
|
|
259
|
+
### `MySQL(SQLDbWrpr)`
|
|
260
|
+
|
|
261
|
+
- `__init__(p_host_name, p_user_name, p_password, p_user_rights, p_recreate_db, p_db_name, p_db_structure, p_sqlalchemy_base, p_sqlalchemy_metadata, p_batch_size, p_bar_len, p_msg_width, p_verbose, p_admin_username, p_admin_user_password, p_db_port, **kwargs)`
|
|
262
|
+
- Opens a MySQL connection and cursor, optionally recreates database/tables, or selects the configured DB.
|
|
263
|
+
- Includes fallback logic to create and grant rights to missing users when valid admin credentials and rights data are supplied.
|
|
264
|
+
|
|
265
|
+
### `PostgreSQL(SQLDbWrpr)`
|
|
266
|
+
|
|
267
|
+
- `__init__(p_host_name, p_user_name, p_password, p_recreate_db, p_db_name, p_db_structure, p_sqlalchemy_base, p_sqlalchemy_metadata, p_batch_size, p_bar_len, p_msg_width, p_verbose, p_db_port, p_maintenance_db, **kwargs)`
|
|
268
|
+
- Configures PostgreSQL-specific behavior (quoted identifiers, non-inline indexes), opens connection, and optionally recreates DB/tables.
|
|
269
|
+
- `build_column_sql(p_field_name, p_field_type, p_field_params, p_field_comment)`
|
|
270
|
+
- PostgreSQL-specific column SQL renderer; handles non-AI `NOT NULL` and defaults.
|
|
271
|
+
- `build_index_sql(p_table_name, p_idx_name, p_idx_fields, p_unique=False)`
|
|
272
|
+
- Builds PostgreSQL `CREATE INDEX`/`CREATE UNIQUE INDEX` statements.
|
|
273
|
+
- `build_insert_sql(p_table_name, p_header, p_replace=False)`
|
|
274
|
+
- Builds PostgreSQL `INSERT`; when `p_replace=True`, builds `ON CONFLICT` upsert/no-op behavior from primary-key metadata.
|
|
275
|
+
- `create_db()`
|
|
276
|
+
- PostgreSQL DB recreation flow via `pg_database` lookup, optional forced drop, create, and reconnect.
|
|
277
|
+
- `create_users(p_admin_user, p_new_users)`
|
|
278
|
+
- Creates PostgreSQL login roles that do not already exist, using the supplied passwords.
|
|
279
|
+
- `delete_users(p_admin_user, p_del_users)`
|
|
280
|
+
- Drops PostgreSQL roles that currently exist.
|
|
281
|
+
- `grant_rights(p_admin_user, p_user_rights)`
|
|
282
|
+
- Grants database-level rights when the table target is `*`; otherwise grants table-level rights. PostgreSQL ignores the host field in each rights entry.
|
|
283
|
+
- `render_default_sql(p_field_type, p_default_value)`
|
|
284
|
+
- Renders PostgreSQL defaults with proper escaping for string values.
|
|
285
|
+
- `render_field_type(p_field_type, p_field_params)`
|
|
286
|
+
- Maps legacy types to PostgreSQL types, including `SERIAL`/`BIGSERIAL` for autoincrement fields.
|
|
287
|
+
- `_connect(p_db_name)`
|
|
288
|
+
- Internal connector helper returning a `psycopg` connection for the requested database.
|
|
289
|
+
|
|
170
290
|
______________________________________________________________________
|
|
171
291
|
|
|
172
292
|
## Updating ReleaseNotes Instructions
|
|
@@ -29,7 +29,7 @@ When both are supplied, `p_db_structure` takes precedence for backward compatibi
|
|
|
29
29
|
- Import CSV data from files or in-memory rows, including single-volume and numbered multi-volume files.
|
|
30
30
|
- Export full tables or custom SQL query results to CSV, including optional multi-volume exports.
|
|
31
31
|
- **Database Support**: Includes MySQL and PostgreSQL wrappers with dialect-specific SQL rendering.
|
|
32
|
-
- **User and Permission Management**: Create MySQL users and grant database rights.
|
|
32
|
+
- **User and Permission Management**: Create and delete MySQL users or PostgreSQL login roles, and grant backend-specific database or table rights.
|
|
33
33
|
- **Batch Processing**: Configure import batch sizes for larger CSV loads.
|
|
34
34
|
|
|
35
35
|
### Project Structure
|
|
@@ -47,6 +47,17 @@ When both are supplied, `p_db_structure` takes precedence for backward compatibi
|
|
|
47
47
|
pip install SqlDbWrpr
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
### Running the Tests
|
|
51
|
+
|
|
52
|
+
The focused database tests use temporary MySQL 8.0 and PostgreSQL 16 Docker containers. Ensure Docker is running, then install the development dependencies and run the test suite:
|
|
53
|
+
|
|
54
|
+
```powershell
|
|
55
|
+
poetry install --with dev
|
|
56
|
+
poetry run pytest
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The test fixtures wait for each database to become ready and remove the temporary containers after the test session.
|
|
60
|
+
|
|
50
61
|
### Quick Start With A Legacy Structure
|
|
51
62
|
|
|
52
63
|
```python
|
|
@@ -131,6 +142,115 @@ db = PostgreSQL(
|
|
|
131
142
|
db.import_csv("Users", p_csv_db=[("Username",), ("alice",)], p_vol_type="Single")
|
|
132
143
|
```
|
|
133
144
|
|
|
145
|
+
## Function Reference
|
|
146
|
+
|
|
147
|
+
This section documents the callable API in `src/sqldbwrpr/sqldbwrpr.py`.
|
|
148
|
+
|
|
149
|
+
### `SchemaSourceError`
|
|
150
|
+
|
|
151
|
+
- `SchemaSourceError(ValueError)`: Raised when no usable schema source is provided to initialize a wrapper (`p_db_structure`, `p_sqlalchemy_metadata`, or `p_sqlalchemy_base.metadata`).
|
|
152
|
+
|
|
153
|
+
### `SQLDbWrpr` (base wrapper)
|
|
154
|
+
|
|
155
|
+
- `__init__(p_host_name, p_user_name, p_password, p_recreate_db, p_db_name, p_db_structure, p_sqlalchemy_base, p_sqlalchemy_metadata, p_batch_size, p_bar_len, p_msg_width, p_verbose, p_db_port, p_ssl_ca, p_ssl_key, p_ssl_cert)`
|
|
156
|
+
- Initializes shared wrapper state, resolves schema source, configures import/export behavior, and precomputes char vs non-char field maps.
|
|
157
|
+
- Schema resolution order: explicit `p_db_structure` first, then SQLAlchemy metadata, else raises `SchemaSourceError`.
|
|
158
|
+
- `close()`
|
|
159
|
+
- Closes the active DB connection if present.
|
|
160
|
+
- `build_column_sql(p_field_name, p_field_type, p_field_params, p_field_comment)`
|
|
161
|
+
- Builds the column fragment for `CREATE TABLE` in the active dialect (base implementation is MySQL-oriented).
|
|
162
|
+
- Applies `AUTO_INCREMENT`, `UNSIGNED`, `NOT NULL`, `ZEROFILL`, defaults, and comments based on field parameters.
|
|
163
|
+
- `build_index_sql(p_table_name, p_idx_name, p_idx_fields, p_unique=False)`
|
|
164
|
+
- Builds an index clause with sort directions from legacy index metadata.
|
|
165
|
+
- `build_insert_sql(p_table_name, p_header, p_replace=False)`
|
|
166
|
+
- Builds an `INSERT` or `REPLACE` statement with backend parameter placeholders.
|
|
167
|
+
- `param_placeholder()`
|
|
168
|
+
- Returns the DB-API parameter token used by execution (`%s`).
|
|
169
|
+
- `quote_identifier(p_identifier)`
|
|
170
|
+
- Quotes SQL identifiers when a backend quote character is configured; otherwise returns plain identifier text.
|
|
171
|
+
- `quote_identifier_list(p_identifiers)`
|
|
172
|
+
- Quotes and joins a list of identifiers using commas.
|
|
173
|
+
- `render_default_sql(p_field_type, p_default_value)`
|
|
174
|
+
- Renders a `DEFAULT` clause, quoting string-like defaults for char/varchar types.
|
|
175
|
+
- `render_field_type(p_field_type, p_field_params)`
|
|
176
|
+
- Renders legacy type arrays into SQL type declarations (e.g., `varchar(n)`, `decimal(p,s)`).
|
|
177
|
+
- `create_db()`
|
|
178
|
+
- Base database recreation flow: drops an existing DB and creates/selects a fresh one.
|
|
179
|
+
- `create_tables()`
|
|
180
|
+
- Validates legacy schema metadata and creates tables, keys, indexes, and constraints in dependency order.
|
|
181
|
+
- Splits table creation and post-create operations where backend rules require deferred statements.
|
|
182
|
+
- `create_users(p_admin_user, p_new_users)`
|
|
183
|
+
- Creates MySQL users if they do not already exist.
|
|
184
|
+
- `delete_users(p_admin_user, p_del_users)`
|
|
185
|
+
- Drops MySQL users that exist in `mysql.user`.
|
|
186
|
+
- `_err_broken_rec(p_sql_str, p_csv_db_slice)`
|
|
187
|
+
- Retry helper used during imports to isolate/log failing rows and stop on first unrecoverable record.
|
|
188
|
+
- `export_to_csv(p_csv_path, p_table_name, p_delimiter="|", p_strip_chars="", p__vol_size=0, p_sql_query="")`
|
|
189
|
+
- Exports table/query results to CSV in single-file or multi-volume mode.
|
|
190
|
+
- Multi-volume mode chunks output by record count and generates numbered files.
|
|
191
|
+
- `get_db_field_types()`
|
|
192
|
+
- Populates per-table lists of character vs non-character columns for type-sensitive import handling.
|
|
193
|
+
- `grant_rights(p_admin_user, p_user_rights)`
|
|
194
|
+
- Grants MySQL rights and corresponding grant-option privileges according to supplied rights tuples.
|
|
195
|
+
- `import_csv(p_table_name, p_csv_file_name="", p_key="", p_header="", p_del_head=False, p_csv_db="", p_csv_corr_str_file_name="", p_vol_type="Multi", p_verbose=False, p_replace=False)`
|
|
196
|
+
- Imports CSV data from file(s) or in-memory rows.
|
|
197
|
+
- Supports single- and multi-volume input, optional header override/removal, correction-string preprocessing, type conversion, date normalization, and batched insert/replace.
|
|
198
|
+
- `import_and_split_csv(p_split_struct, p_data, p_header="", p_insert_header=False, p_verbose=False, p_debug=False)`
|
|
199
|
+
- Splits an input dataset into multiple destination table payloads using declarative field mappings and transform commands, then imports each generated dataset.
|
|
200
|
+
- `from_sqlalchemy_metadata(p_sqlalchemy_metadata)` (`@classmethod`)
|
|
201
|
+
- Converts SQLAlchemy `MetaData` (sorted tables) to the legacy `db_structure` dictionary.
|
|
202
|
+
- `resolve_db_structure(p_db_structure=None, p_sqlalchemy_base=None, p_sqlalchemy_metadata=None)` (`@staticmethod`)
|
|
203
|
+
- Central schema source resolver used by constructors; raises `SchemaSourceError` when none are provided.
|
|
204
|
+
- `_action_to_legacy_code(p_action)` (`@staticmethod`)
|
|
205
|
+
- Maps SQLAlchemy FK action text (e.g., `CASCADE`, `SET NULL`) to legacy action codes (`C`, `N`, etc.).
|
|
206
|
+
- `_build_default_field_params()` (`@staticmethod`)
|
|
207
|
+
- Produces the baseline legacy field-parameter block used during SQLAlchemy conversion.
|
|
208
|
+
- `_column_type_to_legacy(p_column)` (`@staticmethod`)
|
|
209
|
+
- Converts a SQLAlchemy column type object into the legacy type-array format.
|
|
210
|
+
- `_column_to_legacy_field(p_column)` (`@staticmethod`)
|
|
211
|
+
- Converts one SQLAlchemy column into a legacy field definition, including nullability, autoincrement, defaults, and comments.
|
|
212
|
+
- `_set_foreign_keys(p_table, p_table_structure)` (`@staticmethod`)
|
|
213
|
+
- Writes legacy foreign-key metadata onto converted table fields.
|
|
214
|
+
- `_set_indexes(p_table, p_table_structure)` (`@staticmethod`)
|
|
215
|
+
- Writes legacy index metadata for converted table fields.
|
|
216
|
+
- `_set_primary_key(p_table, p_table_structure)` (`@staticmethod`)
|
|
217
|
+
- Marks primary-key columns in converted legacy table definitions.
|
|
218
|
+
- `_table_to_legacy_structure(p_table)` (`@staticmethod`)
|
|
219
|
+
- Converts an entire SQLAlchemy table into a legacy table structure and enriches it with keys/indexes.
|
|
220
|
+
- `_print_err_msg(p_err, p_msg="")` (`@staticmethod`)
|
|
221
|
+
- Formats and prints database error details before termination paths.
|
|
222
|
+
|
|
223
|
+
### `MySQL(SQLDbWrpr)`
|
|
224
|
+
|
|
225
|
+
- `__init__(p_host_name, p_user_name, p_password, p_user_rights, p_recreate_db, p_db_name, p_db_structure, p_sqlalchemy_base, p_sqlalchemy_metadata, p_batch_size, p_bar_len, p_msg_width, p_verbose, p_admin_username, p_admin_user_password, p_db_port, **kwargs)`
|
|
226
|
+
- Opens a MySQL connection and cursor, optionally recreates database/tables, or selects the configured DB.
|
|
227
|
+
- Includes fallback logic to create and grant rights to missing users when valid admin credentials and rights data are supplied.
|
|
228
|
+
|
|
229
|
+
### `PostgreSQL(SQLDbWrpr)`
|
|
230
|
+
|
|
231
|
+
- `__init__(p_host_name, p_user_name, p_password, p_recreate_db, p_db_name, p_db_structure, p_sqlalchemy_base, p_sqlalchemy_metadata, p_batch_size, p_bar_len, p_msg_width, p_verbose, p_db_port, p_maintenance_db, **kwargs)`
|
|
232
|
+
- Configures PostgreSQL-specific behavior (quoted identifiers, non-inline indexes), opens connection, and optionally recreates DB/tables.
|
|
233
|
+
- `build_column_sql(p_field_name, p_field_type, p_field_params, p_field_comment)`
|
|
234
|
+
- PostgreSQL-specific column SQL renderer; handles non-AI `NOT NULL` and defaults.
|
|
235
|
+
- `build_index_sql(p_table_name, p_idx_name, p_idx_fields, p_unique=False)`
|
|
236
|
+
- Builds PostgreSQL `CREATE INDEX`/`CREATE UNIQUE INDEX` statements.
|
|
237
|
+
- `build_insert_sql(p_table_name, p_header, p_replace=False)`
|
|
238
|
+
- Builds PostgreSQL `INSERT`; when `p_replace=True`, builds `ON CONFLICT` upsert/no-op behavior from primary-key metadata.
|
|
239
|
+
- `create_db()`
|
|
240
|
+
- PostgreSQL DB recreation flow via `pg_database` lookup, optional forced drop, create, and reconnect.
|
|
241
|
+
- `create_users(p_admin_user, p_new_users)`
|
|
242
|
+
- Creates PostgreSQL login roles that do not already exist, using the supplied passwords.
|
|
243
|
+
- `delete_users(p_admin_user, p_del_users)`
|
|
244
|
+
- Drops PostgreSQL roles that currently exist.
|
|
245
|
+
- `grant_rights(p_admin_user, p_user_rights)`
|
|
246
|
+
- Grants database-level rights when the table target is `*`; otherwise grants table-level rights. PostgreSQL ignores the host field in each rights entry.
|
|
247
|
+
- `render_default_sql(p_field_type, p_default_value)`
|
|
248
|
+
- Renders PostgreSQL defaults with proper escaping for string values.
|
|
249
|
+
- `render_field_type(p_field_type, p_field_params)`
|
|
250
|
+
- Maps legacy types to PostgreSQL types, including `SERIAL`/`BIGSERIAL` for autoincrement fields.
|
|
251
|
+
- `_connect(p_db_name)`
|
|
252
|
+
- Internal connector helper returning a `psycopg` connection for the requested database.
|
|
253
|
+
|
|
134
254
|
______________________________________________________________________
|
|
135
255
|
|
|
136
256
|
## Updating ReleaseNotes Instructions
|
|
@@ -17,13 +17,7 @@ import mysql.connector
|
|
|
17
17
|
import psycopg
|
|
18
18
|
from beetools import msg as bm
|
|
19
19
|
from mysql.connector import errorcode
|
|
20
|
-
|
|
21
|
-
# from pathlib import Path
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
# _PROJ_DESC = __doc__.split("\n")[0]
|
|
25
|
-
# _PROJ_PATH = Path(__file__)
|
|
26
|
-
# _PROJ_NAME = _PROJ_PATH.stem
|
|
20
|
+
from psycopg import sql as psycopg_sql
|
|
27
21
|
|
|
28
22
|
|
|
29
23
|
class SchemaSourceError(ValueError):
|
|
@@ -52,7 +46,7 @@ class SQLDbWrpr:
|
|
|
52
46
|
p_ssl_key=None,
|
|
53
47
|
p_ssl_cert=None,
|
|
54
48
|
):
|
|
55
|
-
"""
|
|
49
|
+
"""Initialise a common wrapper state and resolve the database schema.
|
|
56
50
|
|
|
57
51
|
A legacy `p_db_structure` dictionary takes precedence. If it is not
|
|
58
52
|
supplied, SQLAlchemy metadata is read from `p_sqlalchemy_metadata` or
|
|
@@ -101,6 +95,7 @@ class SQLDbWrpr:
|
|
|
101
95
|
self.inline_indexes = True
|
|
102
96
|
self.msg_width = p_msg_width
|
|
103
97
|
self.non_char_fields = {}
|
|
98
|
+
self.owns_connection = True
|
|
104
99
|
self._password = p_password
|
|
105
100
|
self.re_create_db = p_recreate_db
|
|
106
101
|
self.silent = p_verbose
|
|
@@ -110,10 +105,16 @@ class SQLDbWrpr:
|
|
|
110
105
|
self.get_db_field_types()
|
|
111
106
|
self.db_port = p_db_port
|
|
112
107
|
|
|
108
|
+
def _execute_import_rows(self, p_table_name, p_header, p_rows, p_replace):
|
|
109
|
+
"""Execute one prepared import batch without managing its transaction."""
|
|
110
|
+
self.cur.executemany(self.build_insert_sql(p_table_name, p_header, p_replace), p_rows)
|
|
111
|
+
|
|
113
112
|
def close(self):
|
|
114
|
-
"""Close
|
|
115
|
-
if self.conn:
|
|
113
|
+
"""Close owned connections, or just this wrapper's cursor on a borrowed connection."""
|
|
114
|
+
if self.conn and self.owns_connection:
|
|
116
115
|
self.conn.close()
|
|
116
|
+
elif self.cur:
|
|
117
|
+
self.cur.close()
|
|
117
118
|
|
|
118
119
|
def build_column_sql(self, p_field_name, p_field_type, p_field_params, p_field_comment):
|
|
119
120
|
"""Build SQL for one column in a CREATE TABLE statement."""
|
|
@@ -548,40 +549,28 @@ class SQLDbWrpr:
|
|
|
548
549
|
return success
|
|
549
550
|
|
|
550
551
|
def create_users(self, p_admin_user, p_new_users):
|
|
551
|
-
"""Create
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
try:
|
|
558
|
-
self.cur.execute(
|
|
559
|
-
"CREATE USER IF NOT EXISTS '{}'@'{}' IDENTIFIED BY '{}'".format(
|
|
560
|
-
user[0], self.host_name, user[1]
|
|
561
|
-
)
|
|
562
|
-
)
|
|
563
|
-
except self.db_error as err:
|
|
564
|
-
self._print_err_msg(err, "Could not create user")
|
|
565
|
-
self.close()
|
|
566
|
-
sys.exit()
|
|
567
|
-
self.conn.commit()
|
|
568
|
-
self.success = True
|
|
552
|
+
"""Create database users.
|
|
553
|
+
|
|
554
|
+
Concrete database backends must implement this because user management
|
|
555
|
+
SQL differs between databases.
|
|
556
|
+
"""
|
|
557
|
+
raise NotImplementedError(f"{self.__class__.__name__} must implement create_users()")
|
|
569
558
|
|
|
570
559
|
def delete_users(self, p_admin_user, p_del_users):
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
self.
|
|
560
|
+
"""Delete database users.
|
|
561
|
+
|
|
562
|
+
Concrete database backends must implement this because user management
|
|
563
|
+
SQL differs between databases.
|
|
564
|
+
"""
|
|
565
|
+
raise NotImplementedError(f"{self.__class__.__name__} must implement delete_users()")
|
|
566
|
+
|
|
567
|
+
def grant_rights(self, p_admin_user, p_user_rights):
|
|
568
|
+
"""Grant database rights.
|
|
569
|
+
|
|
570
|
+
Concrete database backends must implement this because privilege syntax
|
|
571
|
+
differs between databases.
|
|
572
|
+
"""
|
|
573
|
+
raise NotImplementedError(f"{self.__class__.__name__} must implement grant_rights()")
|
|
585
574
|
|
|
586
575
|
def _err_broken_rec(self, p_sql_str, p_csv_db_slice):
|
|
587
576
|
"""Write broken record to logger"""
|
|
@@ -609,11 +598,11 @@ class SQLDbWrpr:
|
|
|
609
598
|
"""Export a table to a csv file
|
|
610
599
|
|
|
611
600
|
Parameters
|
|
612
|
-
- p_csv_path
|
|
613
|
-
- p_table_name = ''
|
|
614
|
-
- p_delimeter = '|'
|
|
601
|
+
- p_csv_path - Path name of the file to be exported
|
|
602
|
+
- p_table_name = '' - Table name to export
|
|
603
|
+
- p_delimeter = '|' - Field delimiter to use
|
|
615
604
|
- p_strip_chars = '' - characters to strip from text
|
|
616
|
-
- p__vol_size = 0
|
|
605
|
+
- p__vol_size = 0 - Create a multiple-volume export. p__vol_size is
|
|
617
606
|
the number of records per file. 0 will create
|
|
618
607
|
only one volume.
|
|
619
608
|
"""
|
|
@@ -768,41 +757,6 @@ class SQLDbWrpr:
|
|
|
768
757
|
else:
|
|
769
758
|
self.non_char_fields[p_table_name].append(field)
|
|
770
759
|
|
|
771
|
-
def grant_rights(self, p_admin_user, p_user_rights):
|
|
772
|
-
"""Grant configured MySQL rights to users."""
|
|
773
|
-
c_user_name = 0
|
|
774
|
-
# c_password = 1
|
|
775
|
-
c_host = 1
|
|
776
|
-
c_db = 2
|
|
777
|
-
c_table = 3
|
|
778
|
-
c_rights = 4
|
|
779
|
-
# success = True
|
|
780
|
-
for right in p_user_rights:
|
|
781
|
-
try:
|
|
782
|
-
sql_str = "GRANT {} ON {}.{} TO '{}'@'{}'".format(
|
|
783
|
-
",".join(right[c_rights:]),
|
|
784
|
-
right[c_db],
|
|
785
|
-
right[c_table],
|
|
786
|
-
right[c_user_name],
|
|
787
|
-
right[c_host],
|
|
788
|
-
)
|
|
789
|
-
self.cur.execute(sql_str)
|
|
790
|
-
self.conn.commit()
|
|
791
|
-
sql_str = "GRANT {} ON {}.{} TO '{}'@'{}' WITH GRANT OPTION".format(
|
|
792
|
-
",".join(right[c_rights:]),
|
|
793
|
-
right[c_db],
|
|
794
|
-
right[c_table],
|
|
795
|
-
right[c_user_name],
|
|
796
|
-
right[c_host],
|
|
797
|
-
)
|
|
798
|
-
self.cur.execute(sql_str)
|
|
799
|
-
self.conn.commit()
|
|
800
|
-
except self.db_error as err:
|
|
801
|
-
self._print_err_msg(err)
|
|
802
|
-
self.close()
|
|
803
|
-
sys.exit()
|
|
804
|
-
self.success = True
|
|
805
|
-
|
|
806
760
|
def import_csv(
|
|
807
761
|
self,
|
|
808
762
|
p_table_name,
|
|
@@ -815,6 +769,7 @@ class SQLDbWrpr:
|
|
|
815
769
|
p_vol_type="Multi",
|
|
816
770
|
p_verbose=False,
|
|
817
771
|
p_replace=False,
|
|
772
|
+
p_manage_transaction=True,
|
|
818
773
|
):
|
|
819
774
|
"""Import CSV rows into a database table.
|
|
820
775
|
|
|
@@ -843,7 +798,11 @@ class SQLDbWrpr:
|
|
|
843
798
|
- p_replace = False
|
|
844
799
|
- False - INSERT into database
|
|
845
800
|
- True - REPLACE into database
|
|
801
|
+
- p_manage_transaction = True
|
|
802
|
+
- False leaves commits and rollbacks to the caller and propagates database errors.
|
|
846
803
|
"""
|
|
804
|
+
if not p_manage_transaction and self.conn.autocommit:
|
|
805
|
+
raise ValueError("Caller-managed imports require autocommit=False")
|
|
847
806
|
|
|
848
807
|
def import_volume(p_csv_db, p_header, p_verbose):
|
|
849
808
|
"""Prepare and write one in-memory CSV volume to the active table."""
|
|
@@ -958,6 +917,12 @@ class SQLDbWrpr:
|
|
|
958
917
|
|
|
959
918
|
def write_to_table(p_csv_db):
|
|
960
919
|
"""Write prepared rows to the destination table."""
|
|
920
|
+
if not p_manage_transaction:
|
|
921
|
+
for start in range(1, len(p_csv_db), self.batch_size):
|
|
922
|
+
self._execute_import_rows(
|
|
923
|
+
p_table_name, header, p_csv_db[start : start + self.batch_size], p_replace
|
|
924
|
+
)
|
|
925
|
+
return
|
|
961
926
|
i = 1
|
|
962
927
|
j = 0 # In case batch size is more than all records
|
|
963
928
|
list_len = len(p_csv_db)
|
|
@@ -1085,6 +1050,7 @@ class SQLDbWrpr:
|
|
|
1085
1050
|
p_insert_header=False,
|
|
1086
1051
|
p_verbose=False,
|
|
1087
1052
|
p_debug=False,
|
|
1053
|
+
p_manage_transaction=True,
|
|
1088
1054
|
):
|
|
1089
1055
|
"""Split CSV rows into one or more destination table imports.
|
|
1090
1056
|
|
|
@@ -1247,6 +1213,7 @@ class SQLDbWrpr:
|
|
|
1247
1213
|
p_header=new_header,
|
|
1248
1214
|
p_verbose=p_verbose,
|
|
1249
1215
|
p_replace=p_split_struct[seq]["Replace"],
|
|
1216
|
+
p_manage_transaction=p_manage_transaction,
|
|
1250
1217
|
)
|
|
1251
1218
|
|
|
1252
1219
|
@classmethod
|
|
@@ -1532,6 +1499,77 @@ class MySQL(SQLDbWrpr):
|
|
|
1532
1499
|
self.success = True
|
|
1533
1500
|
pass
|
|
1534
1501
|
|
|
1502
|
+
def create_users(self, p_admin_user, p_new_users):
|
|
1503
|
+
"""Create MySQL users that do not already exist."""
|
|
1504
|
+
c_user_name = 0
|
|
1505
|
+
self.cur.execute("SELECT User, Host FROM mysql.user")
|
|
1506
|
+
curr_users = self.cur.fetchall()
|
|
1507
|
+
for user in p_new_users:
|
|
1508
|
+
user_key = (user[c_user_name], self.host_name)
|
|
1509
|
+
if user_key not in curr_users:
|
|
1510
|
+
try:
|
|
1511
|
+
self.cur.execute(
|
|
1512
|
+
"CREATE USER IF NOT EXISTS '{}'@'{}' IDENTIFIED BY '{}'".format(
|
|
1513
|
+
user[0], self.host_name, user[1]
|
|
1514
|
+
)
|
|
1515
|
+
)
|
|
1516
|
+
except self.db_error as err:
|
|
1517
|
+
self._print_err_msg(err, "Could not create user")
|
|
1518
|
+
self.close()
|
|
1519
|
+
sys.exit()
|
|
1520
|
+
self.conn.commit()
|
|
1521
|
+
self.success = True
|
|
1522
|
+
|
|
1523
|
+
def delete_users(self, p_admin_user, p_del_users):
|
|
1524
|
+
"""Delete MySQL users that exist."""
|
|
1525
|
+
c_user_name = 0
|
|
1526
|
+
c_host = 2
|
|
1527
|
+
self.cur.execute("SELECT CAST(User AS CHAR) FROM mysql.user")
|
|
1528
|
+
curr_users = [x[0] for x in self.cur.fetchall()]
|
|
1529
|
+
for user in p_del_users:
|
|
1530
|
+
if user[c_user_name] in curr_users:
|
|
1531
|
+
try:
|
|
1532
|
+
self.cur.execute(f"DROP USER '{user[c_user_name]}'@'{user[c_host]}'")
|
|
1533
|
+
except self.db_error as err:
|
|
1534
|
+
self._print_err_msg(err, "Could not delete user")
|
|
1535
|
+
self.close()
|
|
1536
|
+
sys.exit()
|
|
1537
|
+
self.conn.commit()
|
|
1538
|
+
self.success = True
|
|
1539
|
+
|
|
1540
|
+
def grant_rights(self, p_admin_user, p_user_rights):
|
|
1541
|
+
"""Grant configured MySQL rights to users."""
|
|
1542
|
+
c_user_name = 0
|
|
1543
|
+
c_host = 1
|
|
1544
|
+
c_db = 2
|
|
1545
|
+
c_table = 3
|
|
1546
|
+
c_rights = 4
|
|
1547
|
+
for right in p_user_rights:
|
|
1548
|
+
try:
|
|
1549
|
+
sql_str = "GRANT {} ON {}.{} TO '{}'@'{}'".format(
|
|
1550
|
+
",".join(right[c_rights:]),
|
|
1551
|
+
right[c_db],
|
|
1552
|
+
right[c_table],
|
|
1553
|
+
right[c_user_name],
|
|
1554
|
+
right[c_host],
|
|
1555
|
+
)
|
|
1556
|
+
self.cur.execute(sql_str)
|
|
1557
|
+
self.conn.commit()
|
|
1558
|
+
sql_str = "GRANT {} ON {}.{} TO '{}'@'{}' WITH GRANT OPTION".format(
|
|
1559
|
+
",".join(right[c_rights:]),
|
|
1560
|
+
right[c_db],
|
|
1561
|
+
right[c_table],
|
|
1562
|
+
right[c_user_name],
|
|
1563
|
+
right[c_host],
|
|
1564
|
+
)
|
|
1565
|
+
self.cur.execute(sql_str)
|
|
1566
|
+
self.conn.commit()
|
|
1567
|
+
except self.db_error as err:
|
|
1568
|
+
self._print_err_msg(err)
|
|
1569
|
+
self.close()
|
|
1570
|
+
sys.exit()
|
|
1571
|
+
self.success = True
|
|
1572
|
+
|
|
1535
1573
|
|
|
1536
1574
|
class PostgreSQL(SQLDbWrpr):
|
|
1537
1575
|
"""Wrapper for PostgreSQL databases."""
|
|
@@ -1552,9 +1590,19 @@ class PostgreSQL(SQLDbWrpr):
|
|
|
1552
1590
|
p_verbose=False,
|
|
1553
1591
|
p_db_port="5432",
|
|
1554
1592
|
p_maintenance_db="postgres",
|
|
1593
|
+
p_connection=None,
|
|
1555
1594
|
**kwargs,
|
|
1556
1595
|
):
|
|
1557
|
-
"""Create a PostgreSQL wrapper
|
|
1596
|
+
"""Create a PostgreSQL wrapper, optionally borrowing a caller-owned DB-API connection.
|
|
1597
|
+
|
|
1598
|
+
A borrowed connection is never closed or reconfigured. Use
|
|
1599
|
+
p_manage_transaction=False on imports to keep its transaction caller-owned.
|
|
1600
|
+
Database recreation is unavailable on borrowed connections.
|
|
1601
|
+
"""
|
|
1602
|
+
if p_connection is not None and p_recreate_db:
|
|
1603
|
+
raise ValueError("Cannot recreate a database on a borrowed connection")
|
|
1604
|
+
if p_batch_size < 1:
|
|
1605
|
+
raise ValueError("Batch size must be positive")
|
|
1558
1606
|
super().__init__(
|
|
1559
1607
|
p_host_name=p_host_name,
|
|
1560
1608
|
p_user_name=p_user_name,
|
|
@@ -1576,14 +1624,27 @@ class PostgreSQL(SQLDbWrpr):
|
|
|
1576
1624
|
self.maintenance_db = p_maintenance_db
|
|
1577
1625
|
self._connection_kwargs = kwargs
|
|
1578
1626
|
connect_db = self.maintenance_db if self.re_create_db else self.db_name
|
|
1579
|
-
self.
|
|
1580
|
-
self.conn
|
|
1627
|
+
self.owns_connection = p_connection is None
|
|
1628
|
+
self.conn = self._connect(connect_db) if self.owns_connection else p_connection
|
|
1629
|
+
if self.owns_connection:
|
|
1630
|
+
self.conn.autocommit = True
|
|
1581
1631
|
self.cur = self.conn.cursor()
|
|
1582
1632
|
if self.re_create_db:
|
|
1583
1633
|
if self.create_db():
|
|
1584
1634
|
self.create_tables()
|
|
1585
1635
|
self.success = True
|
|
1586
1636
|
|
|
1637
|
+
def _execute_import_rows(self, p_table_name, p_header, p_rows, p_replace):
|
|
1638
|
+
"""Use batched VALUES for psycopg2; retain native executemany for other drivers."""
|
|
1639
|
+
if type(self.conn).__module__.split(".")[0] == "psycopg2":
|
|
1640
|
+
# Borrowing this driver implies the host already has psycopg2 installed.
|
|
1641
|
+
from psycopg2.extras import execute_values
|
|
1642
|
+
|
|
1643
|
+
statement = self.build_insert_sql(p_table_name, p_header, p_replace, p_multi_row=True)
|
|
1644
|
+
execute_values(self.cur, statement, p_rows, page_size=self.batch_size)
|
|
1645
|
+
else:
|
|
1646
|
+
super()._execute_import_rows(p_table_name, p_header, p_rows, p_replace)
|
|
1647
|
+
|
|
1587
1648
|
def build_column_sql(self, p_field_name, p_field_type, p_field_params, p_field_comment):
|
|
1588
1649
|
"""Build PostgreSQL SQL for one column in a CREATE TABLE statement."""
|
|
1589
1650
|
sql_str = f"{self.quote_identifier(p_field_name)} {self.render_field_type(p_field_type, p_field_params)}"
|
|
@@ -1606,12 +1667,13 @@ class PostgreSQL(SQLDbWrpr):
|
|
|
1606
1667
|
",".join(index_fields),
|
|
1607
1668
|
)
|
|
1608
1669
|
|
|
1609
|
-
def build_insert_sql(self, p_table_name, p_header, p_replace=False):
|
|
1670
|
+
def build_insert_sql(self, p_table_name, p_header, p_replace=False, p_multi_row=False):
|
|
1610
1671
|
"""Build a PostgreSQL INSERT or upsert statement."""
|
|
1611
|
-
|
|
1672
|
+
values_sql = "%s" if p_multi_row else f"({','.join(self.param_placeholder() for _ in p_header)})"
|
|
1673
|
+
insert_sql = "INSERT INTO {} ({}) VALUES {}".format(
|
|
1612
1674
|
self.quote_identifier(p_table_name),
|
|
1613
1675
|
",".join([self.quote_identifier(str(x)) for x in p_header]),
|
|
1614
|
-
|
|
1676
|
+
values_sql,
|
|
1615
1677
|
)
|
|
1616
1678
|
if not p_replace:
|
|
1617
1679
|
return insert_sql
|
|
@@ -1685,6 +1747,32 @@ class PostgreSQL(SQLDbWrpr):
|
|
|
1685
1747
|
return f"DECIMAL({str(p_field_type[1])}, {str(p_field_type[2])})"
|
|
1686
1748
|
return type_map.get(field_type, field_type.upper())
|
|
1687
1749
|
|
|
1750
|
+
def update_rows(self, p_table_name, p_header, p_rows, p_key="id"):
|
|
1751
|
+
"""Update supplied columns in bounded batches; the caller owns commit and rollback.
|
|
1752
|
+
|
|
1753
|
+
p_header must contain p_key and at least one update column. Unlike an
|
|
1754
|
+
upsert, this never creates missing records or supplies defaults.
|
|
1755
|
+
"""
|
|
1756
|
+
if p_key not in p_header or len(set(p_header)) != len(p_header) or len(p_header) < 2:
|
|
1757
|
+
raise ValueError("Updates require a unique header containing the key and update columns")
|
|
1758
|
+
if self.conn.autocommit:
|
|
1759
|
+
raise ValueError("Caller-managed updates require autocommit=False")
|
|
1760
|
+
fields = [field for field in p_header if field != p_key]
|
|
1761
|
+
indexes = [p_header.index(field) for field in [*fields, p_key]]
|
|
1762
|
+
assignments = ",".join(f"{self.quote_identifier(field)} = %s" for field in fields)
|
|
1763
|
+
statement = (
|
|
1764
|
+
f"UPDATE {self.quote_identifier(p_table_name)} SET {assignments} "
|
|
1765
|
+
f"WHERE {self.quote_identifier(p_key)} = %s"
|
|
1766
|
+
)
|
|
1767
|
+
for start in range(0, len(p_rows), self.batch_size):
|
|
1768
|
+
rows = [tuple(row[index] for index in indexes) for row in p_rows[start : start + self.batch_size]]
|
|
1769
|
+
if type(self.conn).__module__.split(".")[0] == "psycopg2":
|
|
1770
|
+
from psycopg2.extras import execute_batch
|
|
1771
|
+
|
|
1772
|
+
execute_batch(self.cur, statement, rows, page_size=self.batch_size)
|
|
1773
|
+
else:
|
|
1774
|
+
self.cur.executemany(statement, rows)
|
|
1775
|
+
|
|
1688
1776
|
def _connect(self, p_db_name):
|
|
1689
1777
|
"""Open a PostgreSQL connection."""
|
|
1690
1778
|
return psycopg.connect(
|
|
@@ -1695,3 +1783,81 @@ class PostgreSQL(SQLDbWrpr):
|
|
|
1695
1783
|
port=self.db_port,
|
|
1696
1784
|
**self._connection_kwargs,
|
|
1697
1785
|
)
|
|
1786
|
+
|
|
1787
|
+
def create_users(self, p_admin_user, p_new_users):
|
|
1788
|
+
"""Create PostgreSQL login roles that do not already exist."""
|
|
1789
|
+
c_user_name = 0
|
|
1790
|
+
c_password = 1
|
|
1791
|
+
|
|
1792
|
+
self.cur.execute("SELECT rolname FROM pg_roles WHERE rolcanlogin = true")
|
|
1793
|
+
curr_users = [row[0] for row in self.cur.fetchall()]
|
|
1794
|
+
|
|
1795
|
+
for user in p_new_users:
|
|
1796
|
+
user_name = user[c_user_name]
|
|
1797
|
+
password = user[c_password]
|
|
1798
|
+
|
|
1799
|
+
if user_name not in curr_users:
|
|
1800
|
+
try:
|
|
1801
|
+
self.cur.execute(
|
|
1802
|
+
psycopg_sql.SQL("CREATE USER {} WITH PASSWORD {}").format(
|
|
1803
|
+
psycopg_sql.Identifier(user_name),
|
|
1804
|
+
psycopg_sql.Literal(password),
|
|
1805
|
+
)
|
|
1806
|
+
)
|
|
1807
|
+
except self.db_error as err:
|
|
1808
|
+
self._print_err_msg(err, "Could not create user")
|
|
1809
|
+
self.close()
|
|
1810
|
+
sys.exit()
|
|
1811
|
+
self.success = True
|
|
1812
|
+
|
|
1813
|
+
def delete_users(self, p_admin_user, p_del_users):
|
|
1814
|
+
"""Delete PostgreSQL users that exist."""
|
|
1815
|
+
c_user_name = 0
|
|
1816
|
+
|
|
1817
|
+
self.cur.execute("SELECT rolname FROM pg_roles")
|
|
1818
|
+
curr_users = [row[0] for row in self.cur.fetchall()]
|
|
1819
|
+
|
|
1820
|
+
for user in p_del_users:
|
|
1821
|
+
user_name = user[c_user_name]
|
|
1822
|
+
|
|
1823
|
+
if user_name in curr_users:
|
|
1824
|
+
try:
|
|
1825
|
+
self.cur.execute(f"DROP USER {self.quote_identifier(user_name)}")
|
|
1826
|
+
except self.db_error as err:
|
|
1827
|
+
self._print_err_msg(err, "Could not delete user")
|
|
1828
|
+
self.close()
|
|
1829
|
+
sys.exit()
|
|
1830
|
+
self.success = True
|
|
1831
|
+
|
|
1832
|
+
def grant_rights(self, p_admin_user, p_user_rights):
|
|
1833
|
+
"""Grant configured PostgreSQL rights to users.
|
|
1834
|
+
|
|
1835
|
+
Expected right format:
|
|
1836
|
+
[user_name, host, database, table, right1, right2, ...]
|
|
1837
|
+
The host value is ignored because PostgreSQL does not grant privileges
|
|
1838
|
+
by user/host pair like MySQL.
|
|
1839
|
+
"""
|
|
1840
|
+
c_user_name = 0
|
|
1841
|
+
c_db = 2
|
|
1842
|
+
c_table = 3
|
|
1843
|
+
c_rights = 4
|
|
1844
|
+
|
|
1845
|
+
for right in p_user_rights:
|
|
1846
|
+
try:
|
|
1847
|
+
rights_sql = ",".join(right[c_rights:])
|
|
1848
|
+
user_sql = self.quote_identifier(right[c_user_name])
|
|
1849
|
+
db_sql = self.quote_identifier(right[c_db])
|
|
1850
|
+
table_sql = self.quote_identifier(right[c_table])
|
|
1851
|
+
|
|
1852
|
+
if right[c_table] == "*":
|
|
1853
|
+
sql_str = f"GRANT {rights_sql} ON DATABASE {db_sql} TO {user_sql}"
|
|
1854
|
+
else:
|
|
1855
|
+
sql_str = f"GRANT {rights_sql} ON TABLE {table_sql} TO {user_sql}"
|
|
1856
|
+
|
|
1857
|
+
self.cur.execute(sql_str)
|
|
1858
|
+
self.conn.commit()
|
|
1859
|
+
except self.db_error as err:
|
|
1860
|
+
self._print_err_msg(err)
|
|
1861
|
+
self.close()
|
|
1862
|
+
sys.exit()
|
|
1863
|
+
self.success = True
|
|
File without changes
|
|
File without changes
|