pg-batch 1.2.0__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.
pg_batch/__init__.py ADDED
File without changes
pg_batch/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from . import pg_batch
2
+
3
+ if __name__ == '__main__':
4
+ pg_batch.main()
pg_batch/pg_batch.py ADDED
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # Author: Gabriel Bordeaux (gabfl)
4
+ # Github: https://github.com/gabfl/pg-batch
5
+
6
+ import sys
7
+ import time
8
+ import psycopg2
9
+ import psycopg2.extras
10
+ import argparse
11
+
12
+
13
+ def update_batch(ids, table, set_, sleep=0, primary_key='id'):
14
+ """
15
+ Update a batch of rows
16
+ """
17
+
18
+ global confirmed_write
19
+
20
+ # Leave if ids is empty
21
+ if not ids or len(ids) == 0:
22
+ return None
23
+
24
+ # Prepare update
25
+ print('* Updating %i rows...' % len(ids))
26
+ sql = "UPDATE " + table + " SET " + set_ + \
27
+ " WHERE {0} IN (".format(primary_key) + \
28
+ ', '.join([str(x) for x in ids]) + ")"
29
+ print(" query: " + sql)
30
+
31
+ if confirmed_write or query_yes_no("* Start updating?"):
32
+ # Switch confirmed_write skip the question for the next update
33
+ confirmed_write = True
34
+
35
+ # Execute query
36
+ run_query(sql, sleep)
37
+ else: # answered "no"
38
+ print("Error: Update declined.")
39
+ sys.exit()
40
+
41
+ return True
42
+
43
+
44
+ def delete_batch(ids, table, sleep=0, primary_key='id'):
45
+ """
46
+ Delete a batch of rows
47
+ """
48
+
49
+ global confirmed_write
50
+
51
+ # Leave if ids is empty
52
+ if not ids or len(ids) == 0:
53
+ return None
54
+
55
+ # Prepare delete
56
+ print('* Deleting %i rows...' % len(ids))
57
+ sql = "DELETE FROM " + table + \
58
+ " WHERE {0} IN (".format(primary_key) + \
59
+ ', '.join([str(x) for x in ids]) + ")"
60
+ print(" query: " + sql)
61
+
62
+ if confirmed_write or query_yes_no("* Start deleting?"):
63
+ # Switch confirmed_write skip the question for the next delete
64
+ confirmed_write = True
65
+
66
+ # Execute query
67
+ run_query(sql, sleep)
68
+ else: # answered "no"
69
+ print("Error: Delete declined.")
70
+ sys.exit()
71
+
72
+ return True
73
+
74
+
75
+ def run_query(sql, sleep=0):
76
+ """Execute a write query"""
77
+
78
+ # Execute query
79
+ with connection.cursor() as cursorUpd:
80
+ cursorUpd.execute(sql)
81
+ connection.commit()
82
+
83
+ # Optional Sleep
84
+ if sleep > 0:
85
+ time.sleep(sleep)
86
+
87
+ return True
88
+
89
+
90
+ def get_input():
91
+ """
92
+ Get user input
93
+ """
94
+
95
+ return input().lower()
96
+
97
+
98
+ def query_yes_no(question, default="yes"):
99
+ """Ask a yes/no question via input() and return their answer.
100
+
101
+ "question" is a string that is presented to the user.
102
+ "default" is the presumed answer if the user just hits <Enter>.
103
+ It must be "yes" (the default), "no" or None (meaning
104
+ an answer is required of the user).
105
+
106
+ The "answer" return value is True for "yes" or False for "no".
107
+
108
+ (thanks https://code.activestate.com/recipes/577058/)
109
+ """
110
+ valid = {"yes": True, "y": True, "ye": True,
111
+ "no": False, "n": False}
112
+ if default is None:
113
+ prompt = " [y/n] "
114
+ elif default == "yes":
115
+ prompt = " [Y/n] "
116
+ elif default == "no":
117
+ prompt = " [y/N] "
118
+ else:
119
+ raise ValueError("invalid default answer: '%s'" % default)
120
+
121
+ while True:
122
+ sys.stdout.write(question + prompt)
123
+ choice = get_input()
124
+
125
+ if default is not None and choice == '':
126
+ return valid[default]
127
+ elif choice in valid:
128
+ return valid[choice]
129
+ else:
130
+ sys.stdout.write("Please respond with 'yes' or 'no' "
131
+ "(or 'y' or 'n').\n")
132
+
133
+
134
+ def connect(host, user, port, password, database):
135
+ """
136
+ Connect to a PostgreSQL database
137
+ """
138
+
139
+ # Connect to the database
140
+ try:
141
+ return psycopg2.connect(host=host,
142
+ user=user,
143
+ port=port,
144
+ password=password,
145
+ dbname=database)
146
+ except Exception:
147
+ raise RuntimeError('Error: PostgreSQL connection failed.')
148
+
149
+
150
+ def execute(host, user, port, password, database, action, table, where, set_=None, no_confirm=False, primary_key='id', read_batch_size=10000, write_batch_size=50, sleep=0):
151
+ """
152
+ Execute batch update or delete
153
+ """
154
+
155
+ global confirmed_write, connection
156
+
157
+ # Make sure we have a SET clause for updates
158
+ if action == 'update' and set_ is None:
159
+ raise RuntimeError('Error: argument -s/--set is required for updates.')
160
+
161
+ # Connect to the database
162
+ connection = connect(host, user, port, password, database)
163
+
164
+ try:
165
+ # confirmed_write default value
166
+ confirmed_write = False
167
+ if no_confirm:
168
+ confirmed_write = True
169
+
170
+ with connection.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cursor:
171
+ # Default vars
172
+ min_id = 0
173
+
174
+ while 1: # Infinite loop, will be broken by sys.exit()
175
+ # Get rows to modify
176
+ print("* Selecting data...")
177
+ sql = "SELECT {0} as id FROM ".format(primary_key) + table + " WHERE " + where + \
178
+ " AND {0} > %s ORDER BY {1} LIMIT %s".format(
179
+ primary_key, primary_key)
180
+ print(" query: " + sql % (min_id, read_batch_size))
181
+ cursor.execute(sql, (min_id, read_batch_size))
182
+
183
+ # Row count
184
+ count = cursor.rowcount
185
+
186
+ # No more rows
187
+ if count == 0:
188
+ print("* No more rows to modify!")
189
+ sys.exit()
190
+
191
+ # Loop thru rows
192
+ print("* Preparing to modify %s rows..." % count)
193
+ ids = []
194
+ for result in cursor:
195
+ # Append ID to batch
196
+ ids.append(result.get('id'))
197
+ # print(result)
198
+
199
+ # Minimum ID for future select
200
+ min_id = result.get('id')
201
+
202
+ # Process write when batch size if reached
203
+ if len(ids) >= write_batch_size:
204
+ if action == 'delete':
205
+ # Process delete
206
+ delete_batch(ids, table, sleep, primary_key)
207
+ else:
208
+ # Process update
209
+ update_batch(ids, table, set_, sleep, primary_key)
210
+
211
+ # Reset ids
212
+ ids = []
213
+
214
+ # Process final batch
215
+ if ids:
216
+ if action == 'delete':
217
+ # Process delete
218
+ delete_batch(ids, table, sleep, primary_key)
219
+ else:
220
+ # Process update
221
+ update_batch(ids, table, set_, sleep, primary_key)
222
+ except SystemExit:
223
+ print("* Program exited")
224
+ # except:
225
+ # print("Unexpected error:", sys.exc_info()[0])
226
+ finally:
227
+ connection.close()
228
+
229
+ return True
230
+
231
+
232
+ def main():
233
+ # Parse arguments
234
+ parser = argparse.ArgumentParser()
235
+ parser.add_argument("-H", "--host", default="127.0.0.1",
236
+ help="PostgreSQL server host")
237
+ parser.add_argument("-P", "--port", type=int, default=5432,
238
+ help="PostgreSQL server port")
239
+ parser.add_argument("-U", "--user", required=True,
240
+ help="PostgreSQL user")
241
+ parser.add_argument("-p", "--password", default='',
242
+ help="PostgreSQL password")
243
+ parser.add_argument("-d", "--database", required=True,
244
+ help="PostgreSQL database name")
245
+ parser.add_argument("-t", "--table", required=True,
246
+ help="PostgreSQL table")
247
+ parser.add_argument("-id", "--primary_key", default='id',
248
+ help="Name of the primary key column")
249
+ parser.add_argument("-w", "--where", required=True,
250
+ help="Select WHERE clause")
251
+ parser.add_argument("-s", "--set",
252
+ help="Update SET clause")
253
+ parser.add_argument("-rbz", "--read_batch_size", type=int, default=10000,
254
+ help="Select batch size")
255
+ parser.add_argument("-wbz", "--write_batch_size", type=int, default=50,
256
+ help="Update/delete batch size")
257
+ parser.add_argument("-S", "--sleep", type=float, default=0.00,
258
+ help="Sleep after each batch")
259
+ parser.add_argument("-a", "--action", default='update', choices=['update', 'delete'],
260
+ help="Action ('update' or 'delete')")
261
+ parser.add_argument("-n", "--no_confirm", action='store_true',
262
+ help="Don't ask for confirmation before to run the write queries")
263
+ args = parser.parse_args()
264
+
265
+ execute(host=args.host,
266
+ user=args.user,
267
+ port=args.port,
268
+ password=args.password,
269
+ database=args.database,
270
+ action=args.action,
271
+ table=args.table,
272
+ where=args.where,
273
+ set_=args.set,
274
+ no_confirm=args.no_confirm,
275
+ primary_key=args.primary_key,
276
+ read_batch_size=args.read_batch_size,
277
+ write_batch_size=args.write_batch_size,
278
+ sleep=args.sleep
279
+ )
280
+
281
+
282
+ if __name__ == '__main__':
283
+ main()
@@ -0,0 +1,173 @@
1
+ Metadata-Version: 2.4
2
+ Name: pg_batch
3
+ Version: 1.2.0
4
+ Summary: Run large PostgreSQL UPDATE and DELETE queries with small batches to prevent locks
5
+ Author-email: Gabriel Bordeaux <pypi@gab.lc>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/gabfl/pg-batch
8
+ Project-URL: Issues, https://github.com/gabfl/pg-batch/issues
9
+ Classifier: Topic :: Software Development
10
+ Classifier: Topic :: Database
11
+ Classifier: Topic :: Database :: Database Engines/Servers
12
+ Classifier: Operating System :: MacOS
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Natural Language :: English
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Development Status :: 5 - Production/Stable
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: psycopg2-binary<3,>=2.9
28
+ Dynamic: license-file
29
+
30
+ # pg-batch
31
+
32
+ [![Pypi](https://img.shields.io/pypi/v/pg-batch.svg)](https://pypi.org/project/pg-batch)
33
+ [![Build Status](https://github.com/gabfl/pg-batch/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/gabfl/pg-batch/actions)
34
+ [![codecov](https://codecov.io/gh/gabfl/pg-batch/branch/main/graph/badge.svg)](https://codecov.io/gh/gabfl/pg-batch)
35
+ [![MIT licensed](https://img.shields.io/badge/license-MIT-green.svg)](https://raw.githubusercontent.com/gabfl/pg-batch/main/LICENSE)
36
+
37
+ PostgreSQL equivalent of https://github.com/gabfl/mysql-batch.
38
+
39
+ Updating or deleting a large amount of rows in PostgreSQL will create locks that will paralyze other queries running in parallel.
40
+
41
+ This tool will run UPDATE and DELETE queries in small batches to limit locking. If a large number of rows has to be updated or deleted, it is also possible to limit the number of rows selected at once.
42
+
43
+ ## Installation
44
+
45
+ ```
46
+ pip3 install pg_batch
47
+ ```
48
+
49
+ ## UPDATE example
50
+
51
+ You can run this example with the schema available in [sample_table/schema.sql](sample_table/schema.sql)
52
+
53
+ The following example will be identical to the following update:
54
+
55
+ ```sql
56
+ UPDATE batch_test SET date = NOW() WHERE number > 30 AND date is NULL;
57
+ ```
58
+
59
+ This is the equivalent to process this update with batches of 20 rows:
60
+
61
+ ```bash
62
+ pg_batch --host localhost \
63
+ --user postgres \
64
+ --password secret_password \
65
+ --database "test" \
66
+ --table "batch_test" \
67
+ --write_batch_size 20 \
68
+ --where "number > 30 AND date IS NULL" \
69
+ --set "date = NOW()"
70
+ ```
71
+
72
+ Output sample:
73
+
74
+ ```bash
75
+ * Selecting data...
76
+ query: SELECT id as id FROM batch_test WHERE number > 30 AND date IS NULL AND id > 0 ORDER BY id LIMIT 10000
77
+ * Preparing to modify 70 rows...
78
+ * Updating 20 rows...
79
+ query: UPDATE batch_test SET date = NOW() WHERE id IN (31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50)
80
+ * Start updating? [Y/n]
81
+ * Updating 20 rows...
82
+ query: UPDATE batch_test SET date = NOW() WHERE id IN (51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70)
83
+ * Updating 20 rows...
84
+ query: UPDATE batch_test SET date = NOW() WHERE id IN (71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90)
85
+ * Updating 10 rows...
86
+ query: UPDATE batch_test SET date = NOW() WHERE id IN (91, 92, 93, 94, 95, 96, 97, 98, 99, 100)
87
+ * Selecting data...
88
+ query: SELECT id as id FROM batch_test WHERE number > 30 AND date IS NULL AND id > 100 ORDER BY id LIMIT 10000
89
+ * No more rows to modify!
90
+ * Program exited
91
+ ```
92
+
93
+ ## DELETE example
94
+
95
+ The following example will be identical to the following delete:
96
+
97
+ ```sql
98
+ DELETE FROM batch_test WHERE number > 30 AND date is NULL;
99
+ ```
100
+
101
+ This is the equivalent to process this delete with batches of 20 rows:
102
+
103
+ ```bash
104
+ pg_batch --host localhost \
105
+ --user postgres \
106
+ --password secret_password \
107
+ --database "test" \
108
+ --table "batch_test" \
109
+ --write_batch_size 20 \
110
+ --where "number > 30 AND date IS NULL" \
111
+ --action "delete"
112
+ ```
113
+
114
+ Output sample:
115
+
116
+ ```bash
117
+ * Selecting data...
118
+ query: SELECT id as id FROM batch_test WHERE number > 30 AND date IS NULL AND id > 0 ORDER BY id LIMIT 10000
119
+ * Preparing to modify 70 rows...
120
+ * Deleting 20 rows...
121
+ query: DELETE FROM batch_test WHERE id IN (31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50)
122
+ * Start deleting? [Y/n]
123
+ * Deleting 20 rows...
124
+ query: DELETE FROM batch_test WHERE id IN (51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70)
125
+ * Deleting 20 rows...
126
+ query: DELETE FROM batch_test WHERE id IN (71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90)
127
+ * Deleting 10 rows...
128
+ query: DELETE FROM batch_test WHERE id IN (91, 92, 93, 94, 95, 96, 97, 98, 99, 100)
129
+ * Selecting data...
130
+ query: SELECT id as id FROM batch_test WHERE number > 30 AND date IS NULL AND id > 100 ORDER BY id LIMIT 10000
131
+ * No more rows to modify!
132
+ * Program exited
133
+ ```
134
+
135
+ ## Usage
136
+
137
+ ```bash
138
+ usage: pg_batch [-h] [-H HOST] [-P PORT] -U USER [-p PASSWORD] -d DATABASE -t
139
+ TABLE [-id PRIMARY_KEY] -w WHERE [-s SET]
140
+ [-rbz READ_BATCH_SIZE] [-wbz WRITE_BATCH_SIZE] [-S SLEEP]
141
+ [-a {update,delete}] [-n]
142
+
143
+ options:
144
+ -h, --help show this help message and exit
145
+ -H HOST, --host HOST PostgreSQL server host
146
+ -P PORT, --port PORT PostgreSQL server port
147
+ -U USER, --user USER PostgreSQL user
148
+ -p PASSWORD, --password PASSWORD
149
+ PostgreSQL password
150
+ -d DATABASE, --database DATABASE
151
+ PostgreSQL database name
152
+ -t TABLE, --table TABLE
153
+ PostgreSQL table
154
+ -id PRIMARY_KEY, --primary_key PRIMARY_KEY
155
+ Name of the primary key column
156
+ -w WHERE, --where WHERE
157
+ Select WHERE clause
158
+ -s SET, --set SET Update SET clause
159
+ -rbz READ_BATCH_SIZE, --read_batch_size READ_BATCH_SIZE
160
+ Select batch size
161
+ -wbz WRITE_BATCH_SIZE, --write_batch_size WRITE_BATCH_SIZE
162
+ Update/delete batch size
163
+ -S SLEEP, --sleep SLEEP
164
+ Sleep after each batch
165
+ -a {update,delete}, --action {update,delete}
166
+ Action ('update' or 'delete')
167
+ -n, --no_confirm Don't ask for confirmation before to run the write
168
+ queries
169
+ ```
170
+
171
+ ## License
172
+
173
+ This program is under MIT license ([view license](LICENSE)).
@@ -0,0 +1,9 @@
1
+ pg_batch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ pg_batch/__main__.py,sha256=0XMLusKiaJR-vIpHD3JChZGFhyJL7KNyRO1FTB623XY,71
3
+ pg_batch/pg_batch.py,sha256=MQU2Nz5qvhJUXyhpwbFro5-rUkOnTS7mRWmZhwqIf-c,9034
4
+ pg_batch-1.2.0.dist-info/licenses/LICENSE,sha256=WqS7GgVJk_T1GouVCLKKsR1q83EOah9Hpb72mq0pQ64,1073
5
+ pg_batch-1.2.0.dist-info/METADATA,sha256=R6j7tFu4olPfvEXnDfwPh5KWonY5977Og4k81nMh1Aw,6690
6
+ pg_batch-1.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
7
+ pg_batch-1.2.0.dist-info/entry_points.txt,sha256=cV5U089FFWzVfM7SgVPHqwWjA8gMp7au1SCq_BtNf4M,52
8
+ pg_batch-1.2.0.dist-info/top_level.txt,sha256=MezhyRgDIbdatUfJA5a2GbYU0avk33zBuGqAa2ZKM-w,9
9
+ pg_batch-1.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pg_batch = pg_batch.pg_batch:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Gabriel Bordeaux
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ pg_batch