xbot.plugins.pgsql 0.1.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.
tests/__init__.py ADDED
File without changes
tests/run.py ADDED
@@ -0,0 +1,36 @@
1
+ """
2
+ Run tests against a caller-provided PostgreSQL server.
3
+ """
4
+
5
+ import argparse
6
+ import unittest
7
+ from pathlib import Path
8
+
9
+ from test_pgsql import TestPGSQLConnection
10
+
11
+
12
+ def create_parser() -> argparse.ArgumentParser:
13
+ parser = argparse.ArgumentParser()
14
+ parser.add_argument("-H", "--host", required=True)
15
+ parser.add_argument("-P", "--port", type=int, default=5432)
16
+ parser.add_argument("-u", "--user", required=True)
17
+ parser.add_argument("-p", "--password", required=True)
18
+ parser.add_argument("-d", "--database", required=True)
19
+ return parser
20
+
21
+
22
+ def main() -> int:
23
+ args = create_parser().parse_args()
24
+ TestPGSQLConnection.HOST = args.host
25
+ TestPGSQLConnection.PORT = args.port
26
+ TestPGSQLConnection.USER = args.user
27
+ TestPGSQLConnection.PASSWORD = args.password
28
+ TestPGSQLConnection.DATABASE = args.database
29
+
30
+ suite = unittest.TestLoader().discover(str(Path(__file__).parent))
31
+ result = unittest.TextTestRunner(verbosity=2).run(suite)
32
+ return 0 if result.wasSuccessful() else 1
33
+
34
+
35
+ if __name__ == "__main__":
36
+ raise SystemExit(main())
tests/test_pgsql.py ADDED
@@ -0,0 +1,254 @@
1
+ import unittest
2
+
3
+ import uuid
4
+ from typing import ClassVar
5
+ from unittest.mock import patch
6
+
7
+ from xbot.plugins.pgsql import PGCodes
8
+ from xbot.plugins.pgsql import PGSQLCommandError
9
+ from xbot.plugins.pgsql import PGSQLCommandResult
10
+ from xbot.plugins.pgsql import PGSQLConnectError
11
+ from xbot.plugins.pgsql import PGSQLConnection
12
+
13
+
14
+ class TestPGSQLCommandResult(unittest.TestCase):
15
+ def test_is_list_with_metadata(self) -> None:
16
+ result = PGSQLCommandResult(
17
+ [(1, 'alice'), (2, 'bob')],
18
+ header=('id', 'name'),
19
+ rc='00000',
20
+ cmd='SELECT id, name FROM users ORDER BY id',
21
+ )
22
+
23
+ self.assertIsInstance(result, list)
24
+ self.assertEqual(result, [(1, 'alice'), (2, 'bob')])
25
+ self.assertEqual(result.header, ('id', 'name'))
26
+ self.assertEqual(result.rc, '00000')
27
+ self.assertEqual(result.cmd, 'SELECT id, name FROM users ORDER BY id')
28
+ self.assertEqual(repr(result), "[(1, 'alice'), (2, 'bob')]")
29
+
30
+ def test_str_is_live_psql_table_with_header(self) -> None:
31
+ result = PGSQLCommandResult(
32
+ [(1, 'alice')],
33
+ header=('id', 'name'),
34
+ rc='00000',
35
+ cmd="SELECT 1, 'alice'",
36
+ )
37
+ first_render = str(result)
38
+ self.assertIn('id', first_render)
39
+ self.assertIn('name', first_render)
40
+ self.assertIn('alice', first_render)
41
+
42
+ result.append((2, 'bob'))
43
+ self.assertIn('bob', str(result))
44
+
45
+
46
+ class TestPGSQLConnection(unittest.TestCase):
47
+ HOST: ClassVar[str] = ''
48
+ PORT: ClassVar[int] = 5432
49
+ USER: ClassVar[str] = ''
50
+ PASSWORD: ClassVar[str] = ''
51
+ DATABASE: ClassVar[str] = ''
52
+ connection: ClassVar[PGSQLConnection]
53
+ schema: ClassVar[str] = f'xbot_pgsql_{uuid.uuid4().hex}'
54
+
55
+ @classmethod
56
+ def setUpClass(cls) -> None:
57
+ cls.connection = cls.new_connection()
58
+ cls.connection.exec(f'CREATE SCHEMA "{cls.schema}"')
59
+ cls.connection.exec(f'SET search_path TO "{cls.schema}"')
60
+
61
+ @classmethod
62
+ def tearDownClass(cls) -> None:
63
+ if cls.connection is not None:
64
+ cls.connection.exec(
65
+ f'DROP SCHEMA IF EXISTS "{cls.schema}" CASCADE',
66
+ expect=None,
67
+ )
68
+ cls.connection.disconnect()
69
+
70
+ @classmethod
71
+ def new_connection(cls) -> PGSQLConnection:
72
+ connection = PGSQLConnection()
73
+ connection.connect(
74
+ cls.HOST,
75
+ cls.USER,
76
+ cls.PASSWORD,
77
+ cls.DATABASE,
78
+ cls.PORT,
79
+ )
80
+ return connection
81
+
82
+ def test_connect_is_idempotent(self) -> None:
83
+ connection = self.new_connection()
84
+ try:
85
+ first = connection._conn
86
+ connection.connect(
87
+ self.HOST,
88
+ self.USER,
89
+ self.PASSWORD,
90
+ self.DATABASE,
91
+ self.PORT,
92
+ )
93
+ self.assertIs(connection._conn, first)
94
+ finally:
95
+ connection.disconnect()
96
+
97
+ def test_disconnect_is_idempotent(self) -> None:
98
+ connection = self.new_connection()
99
+ connection.disconnect()
100
+ connection.disconnect()
101
+
102
+ def test_invalid_database_raises_connect_error(self) -> None:
103
+ connection = PGSQLConnection()
104
+ invalid_database = f'xbot_missing_{uuid.uuid4().hex}'
105
+ with self.assertRaises(PGSQLConnectError) as caught:
106
+ connection.connect(
107
+ self.HOST,
108
+ self.USER,
109
+ self.PASSWORD,
110
+ invalid_database,
111
+ self.PORT,
112
+ )
113
+
114
+ def test_exec_select_returns_rows_and_header(self) -> None:
115
+ result = self.connection.exec(
116
+ "SELECT 1::integer AS id, 'alice'::text AS name"
117
+ )
118
+ self.assertEqual(result, [(1, 'alice')])
119
+ self.assertEqual(result.header, ('id', 'name'))
120
+ self.assertEqual(result.rc, '00000')
121
+ self.assertIn('alice', str(result))
122
+
123
+ def test_exec_ddl_dml_returning(self) -> None:
124
+ table = f'items_{uuid.uuid4().hex}'
125
+ ddl = self.connection.exec(
126
+ f'CREATE TABLE "{table}" (id integer, name text)'
127
+ )
128
+ self.assertEqual(ddl, [])
129
+ self.assertEqual(ddl.header, ())
130
+
131
+ inserted = self.connection.exec(
132
+ f"""INSERT INTO "{table}" VALUES (1, 'alice') RETURNING id, name""",
133
+ )
134
+ self.assertEqual(inserted, [(1, 'alice')])
135
+ self.assertEqual(inserted.header, ('id', 'name'))
136
+
137
+ def test_exec_expected_sqlstate_returns_result(self) -> None:
138
+ result = self.connection.exec(
139
+ 'SELECT * FROM missing_table_expected',
140
+ expect=PGCodes.UNDEFINED_TABLE,
141
+ )
142
+ self.assertEqual(result, [])
143
+ self.assertEqual(result.header, ())
144
+ self.assertEqual(result.rc, '42P01')
145
+
146
+ def test_exec_none_expect_accepts_success_and_failure(self) -> None:
147
+ success = self.connection.exec('SELECT 1', expect=None)
148
+ failure = self.connection.exec(
149
+ 'SELECT * FROM missing_table_none',
150
+ expect=None,
151
+ )
152
+ self.assertEqual(success.rc, '00000')
153
+ self.assertEqual(failure.rc, '42P01')
154
+
155
+ def test_exec_default_expect_raises_on_error(self) -> None:
156
+ with self.assertRaises(PGSQLCommandError) as caught:
157
+ self.connection.exec('SELECT * FROM missing_table_default')
158
+
159
+ self.assertEqual(caught.exception.result.rc, '42P01')
160
+ self.assertEqual(caught.exception.expect, PGCodes.SUCCESSFUL_COMPLETION)
161
+
162
+ def test_exec_mismatched_expect_raises(self) -> None:
163
+ with self.assertRaises(PGSQLCommandError) as caught:
164
+ self.connection.exec(
165
+ 'SELECT 1',
166
+ expect=PGCodes.UNDEFINED_TABLE,
167
+ )
168
+ self.assertEqual(caught.exception.result.rc, '00000')
169
+ self.assertIs(caught.exception.expect, PGCodes.UNDEFINED_TABLE)
170
+
171
+ def test_exec_multiple_statements_returns_syntax_error(self) -> None:
172
+ result = self.connection.exec(
173
+ 'SELECT 1; SELECT 2',
174
+ expect=PGCodes.SYNTAX_ERROR,
175
+ )
176
+ self.assertEqual(result.rc, '42601')
177
+
178
+ def test_transaction_commits(self) -> None:
179
+ table = f'tx_commit_{uuid.uuid4().hex}'
180
+ self.connection.exec(
181
+ f'CREATE TABLE "{table}" (id integer PRIMARY KEY)'
182
+ )
183
+
184
+ with self.connection.transaction():
185
+ self.connection.exec(f'INSERT INTO "{table}" VALUES (1)')
186
+
187
+ rows = self.connection.exec(
188
+ f'SELECT id FROM "{table}" ORDER BY id'
189
+ )
190
+ self.assertEqual(rows, [(1,)])
191
+
192
+ def test_transaction_rolls_back_on_exception(self) -> None:
193
+ table = f'tx_rollback_{uuid.uuid4().hex}'
194
+ self.connection.exec(
195
+ f'CREATE TABLE "{table}" (id integer PRIMARY KEY)'
196
+ )
197
+
198
+ with self.assertRaises(RuntimeError):
199
+ with self.connection.transaction():
200
+ self.connection.exec(f'INSERT INTO "{table}" VALUES (1)')
201
+ raise RuntimeError('rollback')
202
+
203
+ rows = self.connection.exec(
204
+ f'SELECT id FROM "{table}" ORDER BY id'
205
+ )
206
+ self.assertEqual(rows, [])
207
+
208
+ def test_nested_transaction_uses_savepoint(self) -> None:
209
+ table = f'nested_{uuid.uuid4().hex}'
210
+ self.connection.exec(
211
+ f'CREATE TABLE "{table}" (id integer PRIMARY KEY)'
212
+ )
213
+
214
+ with self.connection.transaction():
215
+ self.connection.exec(f'INSERT INTO "{table}" VALUES (1)')
216
+ with self.assertRaises(RuntimeError):
217
+ with self.connection.transaction():
218
+ self.connection.exec(
219
+ f'INSERT INTO "{table}" VALUES (2)'
220
+ )
221
+ raise RuntimeError('rollback inner')
222
+ self.connection.exec(f'INSERT INTO "{table}" VALUES (3)')
223
+
224
+ rows = self.connection.exec(
225
+ f'SELECT id FROM "{table}" ORDER BY id'
226
+ )
227
+ self.assertEqual(rows, [(1,), (3,)])
228
+
229
+ def test_complete_public_api(self) -> None:
230
+ self.assertTrue(issubclass(PGSQLCommandError, Exception))
231
+ self.assertTrue(issubclass(PGSQLConnectError, Exception))
232
+ self.assertTrue(issubclass(PGSQLCommandResult, list))
233
+ self.assertTrue(issubclass(PGSQLConnection, object))
234
+ self.assertEqual(PGCodes.SUCCESSFUL_COMPLETION.value, '00000')
235
+
236
+ def test_connect_log_does_not_contain_password(self) -> None:
237
+ connection = PGSQLConnection()
238
+ with patch.object(connection._logger, 'info') as info:
239
+ connection.connect(
240
+ self.HOST,
241
+ self.USER,
242
+ self.PASSWORD,
243
+ self.DATABASE,
244
+ self.PORT,
245
+ )
246
+ try:
247
+ calls = '\n'.join(str(call) for call in info.call_args_list)
248
+ self.assertNotIn(self.PASSWORD, calls)
249
+ finally:
250
+ connection.disconnect()
251
+
252
+
253
+ if __name__ == '__main__':
254
+ unittest.main(verbosity=2)
@@ -0,0 +1,17 @@
1
+ """
2
+ PostgreSQL plugin for xbot.framework.
3
+ """
4
+
5
+ from xbot.plugins.pgsql.codes import PGCodes
6
+ from xbot.plugins.pgsql.errors import PGSQLCommandError, PGSQLConnectError
7
+ from xbot.plugins.pgsql.pgsql import PGSQLCommandResult, PGSQLConnection
8
+ from xbot.plugins.pgsql.version import __version__
9
+
10
+ __all__ = [
11
+ 'PGCodes',
12
+ 'PGSQLCommandError',
13
+ 'PGSQLCommandResult',
14
+ 'PGSQLConnectError',
15
+ 'PGSQLConnection',
16
+ '__version__',
17
+ ]
@@ -0,0 +1,363 @@
1
+ """
2
+ PostgreSQL 14-18 SQLSTATE codes.
3
+ """
4
+
5
+ from enum import Enum, unique
6
+
7
+
8
+ @unique
9
+ class PGCodes(str, Enum):
10
+ """
11
+ PostgreSQL 14-18 official SQLSTATE codes.
12
+ """
13
+
14
+ # Class 00 - Successful Completion
15
+ SUCCESSFUL_COMPLETION = '00000'
16
+
17
+ # Class 01 - Warning
18
+ WARNING = '01000'
19
+ NULL_VALUE_ELIMINATED_IN_SET_FUNCTION = '01003'
20
+ STRING_DATA_RIGHT_TRUNCATION_01 = '01004'
21
+ PRIVILEGE_NOT_REVOKED = '01006'
22
+ PRIVILEGE_NOT_GRANTED = '01007'
23
+ IMPLICIT_ZERO_BIT_PADDING = '01008'
24
+ DYNAMIC_RESULT_SETS_RETURNED = '0100C'
25
+ DEPRECATED_FEATURE = '01P01'
26
+
27
+ # Class 02 - No Data (this is also a warning class per the SQL standard)
28
+ NO_DATA = '02000'
29
+ NO_ADDITIONAL_DYNAMIC_RESULT_SETS_RETURNED = '02001'
30
+
31
+ # Class 03 - SQL Statement Not Yet Complete
32
+ SQL_STATEMENT_NOT_YET_COMPLETE = '03000'
33
+
34
+ # Class 08 - Connection Exception
35
+ CONNECTION_EXCEPTION = '08000'
36
+ SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION = '08001'
37
+ CONNECTION_DOES_NOT_EXIST = '08003'
38
+ SQLSERVER_REJECTED_ESTABLISHMENT_OF_SQLCONNECTION = '08004'
39
+ CONNECTION_FAILURE = '08006'
40
+ TRANSACTION_RESOLUTION_UNKNOWN = '08007'
41
+ PROTOCOL_VIOLATION = '08P01'
42
+
43
+ # Class 09 - Triggered Action Exception
44
+ TRIGGERED_ACTION_EXCEPTION = '09000'
45
+
46
+ # Class 0A - Feature Not Supported
47
+ FEATURE_NOT_SUPPORTED = '0A000'
48
+
49
+ # Class 0B - Invalid Transaction Initiation
50
+ INVALID_TRANSACTION_INITIATION = '0B000'
51
+
52
+ # Class 0F - Locator Exception
53
+ LOCATOR_EXCEPTION = '0F000'
54
+ INVALID_LOCATOR_SPECIFICATION = '0F001'
55
+
56
+ # Class 0L - Invalid Grantor
57
+ INVALID_GRANTOR = '0L000'
58
+ INVALID_GRANT_OPERATION = '0LP01'
59
+
60
+ # Class 0P - Invalid Role Specification
61
+ INVALID_ROLE_SPECIFICATION = '0P000'
62
+
63
+ # Class 0Z - Diagnostics Exception
64
+ DIAGNOSTICS_EXCEPTION = '0Z000'
65
+ STACKED_DIAGNOSTICS_ACCESSED_WITHOUT_ACTIVE_HANDLER = '0Z002'
66
+
67
+ # Class 10 - XQuery Error
68
+ INVALID_ARGUMENT_FOR_XQUERY = '10608'
69
+
70
+ # Class 20 - Case Not Found
71
+ CASE_NOT_FOUND = '20000'
72
+
73
+ # Class 21 - Cardinality Violation
74
+ CARDINALITY_VIOLATION = '21000'
75
+
76
+ # Class 22 - Data Exception
77
+ DATA_EXCEPTION = '22000'
78
+ STRING_DATA_RIGHT_TRUNCATION_22 = '22001'
79
+ NULL_VALUE_NO_INDICATOR_PARAMETER = '22002'
80
+ NUMERIC_VALUE_OUT_OF_RANGE = '22003'
81
+ NULL_VALUE_NOT_ALLOWED_22 = '22004'
82
+ ERROR_IN_ASSIGNMENT = '22005'
83
+ INVALID_DATETIME_FORMAT = '22007'
84
+ DATETIME_FIELD_OVERFLOW = '22008'
85
+ INVALID_TIME_ZONE_DISPLACEMENT_VALUE = '22009'
86
+ ESCAPE_CHARACTER_CONFLICT = '2200B'
87
+ INVALID_USE_OF_ESCAPE_CHARACTER = '2200C'
88
+ INVALID_ESCAPE_OCTET = '2200D'
89
+ ZERO_LENGTH_CHARACTER_STRING = '2200F'
90
+ MOST_SPECIFIC_TYPE_MISMATCH = '2200G'
91
+ SEQUENCE_GENERATOR_LIMIT_EXCEEDED = '2200H'
92
+ NOT_AN_XML_DOCUMENT = '2200L'
93
+ INVALID_XML_DOCUMENT = '2200M'
94
+ INVALID_XML_CONTENT = '2200N'
95
+ INVALID_XML_COMMENT = '2200S'
96
+ INVALID_XML_PROCESSING_INSTRUCTION = '2200T'
97
+ INVALID_INDICATOR_PARAMETER_VALUE = '22010'
98
+ SUBSTRING_ERROR = '22011'
99
+ DIVISION_BY_ZERO = '22012'
100
+ INVALID_PRECEDING_OR_FOLLOWING_SIZE = '22013'
101
+ INVALID_ARGUMENT_FOR_NTILE_FUNCTION = '22014'
102
+ INTERVAL_FIELD_OVERFLOW = '22015'
103
+ INVALID_ARGUMENT_FOR_NTH_VALUE_FUNCTION = '22016'
104
+ INVALID_CHARACTER_VALUE_FOR_CAST = '22018'
105
+ INVALID_ESCAPE_CHARACTER = '22019'
106
+ INVALID_REGULAR_EXPRESSION = '2201B'
107
+ INVALID_ARGUMENT_FOR_LOGARITHM = '2201E'
108
+ INVALID_ARGUMENT_FOR_POWER_FUNCTION = '2201F'
109
+ INVALID_ARGUMENT_FOR_WIDTH_BUCKET_FUNCTION = '2201G'
110
+ INVALID_ROW_COUNT_IN_LIMIT_CLAUSE = '2201W'
111
+ INVALID_ROW_COUNT_IN_RESULT_OFFSET_CLAUSE = '2201X'
112
+ CHARACTER_NOT_IN_REPERTOIRE = '22021'
113
+ INDICATOR_OVERFLOW = '22022'
114
+ INVALID_PARAMETER_VALUE = '22023'
115
+ UNTERMINATED_C_STRING = '22024'
116
+ INVALID_ESCAPE_SEQUENCE = '22025'
117
+ STRING_DATA_LENGTH_MISMATCH = '22026'
118
+ TRIM_ERROR = '22027'
119
+ ARRAY_SUBSCRIPT_ERROR = '2202E'
120
+ INVALID_TABLESAMPLE_REPEAT = '2202G'
121
+ INVALID_TABLESAMPLE_ARGUMENT = '2202H'
122
+ DUPLICATE_JSON_OBJECT_KEY_VALUE = '22030'
123
+ INVALID_ARGUMENT_FOR_SQL_JSON_DATETIME_FUNCTION = '22031'
124
+ INVALID_JSON_TEXT = '22032'
125
+ INVALID_SQL_JSON_SUBSCRIPT = '22033'
126
+ MORE_THAN_ONE_SQL_JSON_ITEM = '22034'
127
+ NO_SQL_JSON_ITEM = '22035'
128
+ NON_NUMERIC_SQL_JSON_ITEM = '22036'
129
+ NON_UNIQUE_KEYS_IN_A_JSON_OBJECT = '22037'
130
+ SINGLETON_SQL_JSON_ITEM_REQUIRED = '22038'
131
+ SQL_JSON_ARRAY_NOT_FOUND = '22039'
132
+ SQL_JSON_MEMBER_NOT_FOUND = '2203A'
133
+ SQL_JSON_NUMBER_NOT_FOUND = '2203B'
134
+ SQL_JSON_OBJECT_NOT_FOUND = '2203C'
135
+ TOO_MANY_JSON_ARRAY_ELEMENTS = '2203D'
136
+ TOO_MANY_JSON_OBJECT_MEMBERS = '2203E'
137
+ SQL_JSON_SCALAR_REQUIRED = '2203F'
138
+ SQL_JSON_ITEM_CANNOT_BE_CAST_TO_TARGET_TYPE = '2203G'
139
+ FLOATING_POINT_EXCEPTION = '22P01'
140
+ INVALID_TEXT_REPRESENTATION = '22P02'
141
+ INVALID_BINARY_REPRESENTATION = '22P03'
142
+ BAD_COPY_FILE_FORMAT = '22P04'
143
+ UNTRANSLATABLE_CHARACTER = '22P05'
144
+ NONSTANDARD_USE_OF_ESCAPE_CHARACTER = '22P06'
145
+
146
+ # Class 23 - Integrity Constraint Violation
147
+ INTEGRITY_CONSTRAINT_VIOLATION = '23000'
148
+ RESTRICT_VIOLATION = '23001'
149
+ NOT_NULL_VIOLATION = '23502'
150
+ FOREIGN_KEY_VIOLATION = '23503'
151
+ UNIQUE_VIOLATION = '23505'
152
+ CHECK_VIOLATION = '23514'
153
+ EXCLUSION_VIOLATION = '23P01'
154
+
155
+ # Class 24 - Invalid Cursor State
156
+ INVALID_CURSOR_STATE = '24000'
157
+
158
+ # Class 25 - Invalid Transaction State
159
+ INVALID_TRANSACTION_STATE = '25000'
160
+ ACTIVE_SQL_TRANSACTION = '25001'
161
+ BRANCH_TRANSACTION_ALREADY_ACTIVE = '25002'
162
+ INAPPROPRIATE_ACCESS_MODE_FOR_BRANCH_TRANSACTION = '25003'
163
+ INAPPROPRIATE_ISOLATION_LEVEL_FOR_BRANCH_TRANSACTION = '25004'
164
+ NO_ACTIVE_SQL_TRANSACTION_FOR_BRANCH_TRANSACTION = '25005'
165
+ READ_ONLY_SQL_TRANSACTION = '25006'
166
+ SCHEMA_AND_DATA_STATEMENT_MIXING_NOT_SUPPORTED = '25007'
167
+ HELD_CURSOR_REQUIRES_SAME_ISOLATION_LEVEL = '25008'
168
+ NO_ACTIVE_SQL_TRANSACTION = '25P01'
169
+ IN_FAILED_SQL_TRANSACTION = '25P02'
170
+ IDLE_IN_TRANSACTION_SESSION_TIMEOUT = '25P03'
171
+ TRANSACTION_TIMEOUT = '25P04'
172
+
173
+ # Class 26 - Invalid SQL Statement Name
174
+ INVALID_SQL_STATEMENT_NAME = '26000'
175
+
176
+ # Class 27 - Triggered Data Change Violation
177
+ TRIGGERED_DATA_CHANGE_VIOLATION = '27000'
178
+
179
+ # Class 28 - Invalid Authorization Specification
180
+ INVALID_AUTHORIZATION_SPECIFICATION = '28000'
181
+ INVALID_PASSWORD = '28P01'
182
+
183
+ # Class 2B - Dependent Privilege Descriptors Still Exist
184
+ DEPENDENT_PRIVILEGE_DESCRIPTORS_STILL_EXIST = '2B000'
185
+ DEPENDENT_OBJECTS_STILL_EXIST = '2BP01'
186
+
187
+ # Class 2D - Invalid Transaction Termination
188
+ INVALID_TRANSACTION_TERMINATION = '2D000'
189
+
190
+ # Class 2F - SQL Routine Exception
191
+ SQL_ROUTINE_EXCEPTION = '2F000'
192
+ MODIFYING_SQL_DATA_NOT_PERMITTED_2F = '2F002'
193
+ PROHIBITED_SQL_STATEMENT_ATTEMPTED_2F = '2F003'
194
+ READING_SQL_DATA_NOT_PERMITTED_2F = '2F004'
195
+ FUNCTION_EXECUTED_NO_RETURN_STATEMENT = '2F005'
196
+
197
+ # Class 34 - Invalid Cursor Name
198
+ INVALID_CURSOR_NAME = '34000'
199
+
200
+ # Class 38 - External Routine Exception
201
+ EXTERNAL_ROUTINE_EXCEPTION = '38000'
202
+ CONTAINING_SQL_NOT_PERMITTED = '38001'
203
+ MODIFYING_SQL_DATA_NOT_PERMITTED_38 = '38002'
204
+ PROHIBITED_SQL_STATEMENT_ATTEMPTED_38 = '38003'
205
+ READING_SQL_DATA_NOT_PERMITTED_38 = '38004'
206
+
207
+ # Class 39 - External Routine Invocation Exception
208
+ EXTERNAL_ROUTINE_INVOCATION_EXCEPTION = '39000'
209
+ INVALID_SQLSTATE_RETURNED = '39001'
210
+ NULL_VALUE_NOT_ALLOWED_39 = '39004'
211
+ TRIGGER_PROTOCOL_VIOLATED = '39P01'
212
+ SRF_PROTOCOL_VIOLATED = '39P02'
213
+ EVENT_TRIGGER_PROTOCOL_VIOLATED = '39P03'
214
+
215
+ # Class 3B - Savepoint Exception
216
+ SAVEPOINT_EXCEPTION = '3B000'
217
+ INVALID_SAVEPOINT_SPECIFICATION = '3B001'
218
+
219
+ # Class 3D - Invalid Catalog Name
220
+ INVALID_CATALOG_NAME = '3D000'
221
+
222
+ # Class 3F - Invalid Schema Name
223
+ INVALID_SCHEMA_NAME = '3F000'
224
+
225
+ # Class 40 - Transaction Rollback
226
+ TRANSACTION_ROLLBACK = '40000'
227
+ SERIALIZATION_FAILURE = '40001'
228
+ TRANSACTION_INTEGRITY_CONSTRAINT_VIOLATION = '40002'
229
+ STATEMENT_COMPLETION_UNKNOWN = '40003'
230
+ DEADLOCK_DETECTED = '40P01'
231
+
232
+ # Class 42 - Syntax Error or Access Rule Violation
233
+ SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION = '42000'
234
+ INSUFFICIENT_PRIVILEGE = '42501'
235
+ SYNTAX_ERROR = '42601'
236
+ INVALID_NAME = '42602'
237
+ INVALID_COLUMN_DEFINITION = '42611'
238
+ NAME_TOO_LONG = '42622'
239
+ DUPLICATE_COLUMN = '42701'
240
+ AMBIGUOUS_COLUMN = '42702'
241
+ UNDEFINED_COLUMN = '42703'
242
+ UNDEFINED_OBJECT = '42704'
243
+ DUPLICATE_OBJECT = '42710'
244
+ DUPLICATE_ALIAS = '42712'
245
+ DUPLICATE_FUNCTION = '42723'
246
+ AMBIGUOUS_FUNCTION = '42725'
247
+ GROUPING_ERROR = '42803'
248
+ DATATYPE_MISMATCH = '42804'
249
+ WRONG_OBJECT_TYPE = '42809'
250
+ INVALID_FOREIGN_KEY = '42830'
251
+ CANNOT_COERCE = '42846'
252
+ UNDEFINED_FUNCTION = '42883'
253
+ GENERATED_ALWAYS = '428C9'
254
+ RESERVED_NAME = '42939'
255
+ UNDEFINED_TABLE = '42P01'
256
+ UNDEFINED_PARAMETER = '42P02'
257
+ DUPLICATE_CURSOR = '42P03'
258
+ DUPLICATE_DATABASE = '42P04'
259
+ DUPLICATE_PREPARED_STATEMENT = '42P05'
260
+ DUPLICATE_SCHEMA = '42P06'
261
+ DUPLICATE_TABLE = '42P07'
262
+ AMBIGUOUS_PARAMETER = '42P08'
263
+ AMBIGUOUS_ALIAS = '42P09'
264
+ INVALID_COLUMN_REFERENCE = '42P10'
265
+ INVALID_CURSOR_DEFINITION = '42P11'
266
+ INVALID_DATABASE_DEFINITION = '42P12'
267
+ INVALID_FUNCTION_DEFINITION = '42P13'
268
+ INVALID_PREPARED_STATEMENT_DEFINITION = '42P14'
269
+ INVALID_SCHEMA_DEFINITION = '42P15'
270
+ INVALID_TABLE_DEFINITION = '42P16'
271
+ INVALID_OBJECT_DEFINITION = '42P17'
272
+ INDETERMINATE_DATATYPE = '42P18'
273
+ INVALID_RECURSION = '42P19'
274
+ WINDOWING_ERROR = '42P20'
275
+ COLLATION_MISMATCH = '42P21'
276
+ INDETERMINATE_COLLATION = '42P22'
277
+
278
+ # Class 44 - WITH CHECK OPTION Violation
279
+ WITH_CHECK_OPTION_VIOLATION = '44000'
280
+
281
+ # Class 53 - Insufficient Resources
282
+ INSUFFICIENT_RESOURCES = '53000'
283
+ DISK_FULL = '53100'
284
+ OUT_OF_MEMORY = '53200'
285
+ TOO_MANY_CONNECTIONS = '53300'
286
+ CONFIGURATION_LIMIT_EXCEEDED = '53400'
287
+
288
+ # Class 54 - Program Limit Exceeded
289
+ PROGRAM_LIMIT_EXCEEDED = '54000'
290
+ STATEMENT_TOO_COMPLEX = '54001'
291
+ TOO_MANY_COLUMNS = '54011'
292
+ TOO_MANY_ARGUMENTS = '54023'
293
+
294
+ # Class 55 - Object Not In Prerequisite State
295
+ OBJECT_NOT_IN_PREREQUISITE_STATE = '55000'
296
+ OBJECT_IN_USE = '55006'
297
+ CANT_CHANGE_RUNTIME_PARAM = '55P02'
298
+ LOCK_NOT_AVAILABLE = '55P03'
299
+ UNSAFE_NEW_ENUM_VALUE_USAGE = '55P04'
300
+
301
+ # Class 57 - Operator Intervention
302
+ OPERATOR_INTERVENTION = '57000'
303
+ QUERY_CANCELED = '57014'
304
+ ADMIN_SHUTDOWN = '57P01'
305
+ CRASH_SHUTDOWN = '57P02'
306
+ CANNOT_CONNECT_NOW = '57P03'
307
+ DATABASE_DROPPED = '57P04'
308
+ IDLE_SESSION_TIMEOUT = '57P05'
309
+
310
+ # Class 58 - System Error (errors external to PostgreSQL itself)
311
+ SYSTEM_ERROR = '58000'
312
+ IO_ERROR = '58030'
313
+ UNDEFINED_FILE = '58P01'
314
+ DUPLICATE_FILE = '58P02'
315
+ FILE_NAME_TOO_LONG = '58P03'
316
+
317
+ # Class 72 - Snapshot Failure
318
+ SNAPSHOT_TOO_OLD = '72000'
319
+
320
+ # Class F0 - Configuration File Error
321
+ CONFIG_FILE_ERROR = 'F0000'
322
+ LOCK_FILE_EXISTS = 'F0001'
323
+
324
+ # Class HV - Foreign Data Wrapper Error (SQL/MED)
325
+ FDW_ERROR = 'HV000'
326
+ FDW_OUT_OF_MEMORY = 'HV001'
327
+ FDW_DYNAMIC_PARAMETER_VALUE_NEEDED = 'HV002'
328
+ FDW_INVALID_DATA_TYPE = 'HV004'
329
+ FDW_COLUMN_NAME_NOT_FOUND = 'HV005'
330
+ FDW_INVALID_DATA_TYPE_DESCRIPTORS = 'HV006'
331
+ FDW_INVALID_COLUMN_NAME = 'HV007'
332
+ FDW_INVALID_COLUMN_NUMBER = 'HV008'
333
+ FDW_INVALID_USE_OF_NULL_POINTER = 'HV009'
334
+ FDW_INVALID_STRING_FORMAT = 'HV00A'
335
+ FDW_INVALID_HANDLE = 'HV00B'
336
+ FDW_INVALID_OPTION_INDEX = 'HV00C'
337
+ FDW_INVALID_OPTION_NAME = 'HV00D'
338
+ FDW_OPTION_NAME_NOT_FOUND = 'HV00J'
339
+ FDW_REPLY_HANDLE = 'HV00K'
340
+ FDW_UNABLE_TO_CREATE_EXECUTION = 'HV00L'
341
+ FDW_UNABLE_TO_CREATE_REPLY = 'HV00M'
342
+ FDW_UNABLE_TO_ESTABLISH_CONNECTION = 'HV00N'
343
+ FDW_NO_SCHEMAS = 'HV00P'
344
+ FDW_SCHEMA_NOT_FOUND = 'HV00Q'
345
+ FDW_TABLE_NOT_FOUND = 'HV00R'
346
+ FDW_FUNCTION_SEQUENCE_ERROR = 'HV010'
347
+ FDW_TOO_MANY_HANDLES = 'HV014'
348
+ FDW_INCONSISTENT_DESCRIPTOR_INFORMATION = 'HV021'
349
+ FDW_INVALID_ATTRIBUTE_VALUE = 'HV024'
350
+ FDW_INVALID_STRING_LENGTH_OR_BUFFER_LENGTH = 'HV090'
351
+ FDW_INVALID_DESCRIPTOR_FIELD_IDENTIFIER = 'HV091'
352
+
353
+ # Class P0 - PL/pgSQL Error
354
+ PLPGSQL_ERROR = 'P0000'
355
+ RAISE_EXCEPTION = 'P0001'
356
+ NO_DATA_FOUND = 'P0002'
357
+ TOO_MANY_ROWS = 'P0003'
358
+ ASSERT_FAILURE = 'P0004'
359
+
360
+ # Class XX - Internal Error
361
+ INTERNAL_ERROR = 'XX000'
362
+ DATA_CORRUPTED = 'XX001'
363
+ INDEX_CORRUPTED = 'XX002'
@@ -0,0 +1,44 @@
1
+ """
2
+ Public exceptions.
3
+ """
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from xbot.plugins.pgsql.codes import PGCodes
9
+ from xbot.plugins.pgsql.pgsql import PGSQLCommandResult
10
+
11
+
12
+ class PGSQLConnectError(Exception):
13
+ """
14
+ Raised when a PostgreSQL connection cannot be established.
15
+
16
+ The original Psycopg exception is preserved via ``__cause__``.
17
+ """
18
+
19
+
20
+ class PGSQLCommandError(Exception):
21
+ """
22
+ Raised when a SQL command result differs from the expectation.
23
+
24
+ Exposes ``.result`` (``PGSQLCommandResult``) and ``.expect``
25
+ (``PGCodes | None``). When the error originates from a caught
26
+ ``psycopg.Error``, the original exception is preserved via
27
+ ``__cause__``.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ message: str,
33
+ *,
34
+ result: 'PGSQLCommandResult',
35
+ expect: 'PGCodes | None',
36
+ ) -> None:
37
+ """
38
+ :param message: error message.
39
+ :param result: command result.
40
+ :param expect: expected SQLSTATE code.
41
+ """
42
+ super().__init__(message)
43
+ self.result = result
44
+ self.expect = expect
@@ -0,0 +1,213 @@
1
+ """
2
+ Synchronous PostgreSQL connection and command result.
3
+ """
4
+
5
+ import textwrap
6
+
7
+ from collections.abc import Iterable, Generator
8
+ from contextlib import contextmanager
9
+ from typing import Any
10
+
11
+ import psycopg
12
+ from psycopg import Connection
13
+ from psycopg.rows import tuple_row
14
+ from xbot.framework.logger import ExtraAdapter, getlogger
15
+
16
+ from cli_helpers import tabular_output
17
+
18
+ from xbot.plugins.pgsql.codes import PGCodes
19
+ from xbot.plugins.pgsql.errors import PGSQLCommandError, PGSQLConnectError
20
+
21
+
22
+ logger = getlogger(__name__)
23
+
24
+
25
+ class PGSQLCommandResult(list[tuple[Any, ...]]):
26
+ """
27
+ Rows and metadata produced by one SQL command.
28
+
29
+ A ``list[tuple[Any, ...]]`` subclass. Each element is a row tuple;
30
+ the object also carries ``header`` (column names), ``rc`` (SQLSTATE,
31
+ ``"00000"`` on success) and ``cmd`` (the SQL command for logging).
32
+ Calling ``str(result)`` renders rows as a psql-style table that
33
+ updates live as rows are appended.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ rows: Iterable[tuple[Any, ...]] = (),
39
+ *,
40
+ header: tuple[str, ...] = (),
41
+ rc: str | None = '00000',
42
+ cmd: str = ''
43
+ ) -> None:
44
+ """
45
+ :param rows: row tuples.
46
+ :param header: column names.
47
+ :param rc: SQLSTATE code.
48
+ :param cmd: SQL command.
49
+ """
50
+ super().__init__(rows)
51
+ self.header = header
52
+ self.rc = rc
53
+ self.cmd = cmd
54
+
55
+ def __str__(self) -> str:
56
+ """
57
+ Render the current rows as a psql-style table.
58
+ """
59
+ lines = tabular_output.format_output(
60
+ self,
61
+ self.header,
62
+ format_name='psql',
63
+ )
64
+ return '\n'.join(lines)
65
+
66
+
67
+ class PGSQLConnection(object):
68
+ """
69
+ One synchronous PostgreSQL connection.
70
+ """
71
+
72
+ def __init__(self) -> None:
73
+ self._conn: Connection[tuple[Any, ...]] | None = None
74
+ self._logger = ExtraAdapter(logger, {})
75
+
76
+ def connect(
77
+ self,
78
+ host: str,
79
+ user: str,
80
+ password: str,
81
+ database: str,
82
+ port: int = 5432,
83
+ timeout: int = 5
84
+ ) -> None:
85
+ """
86
+ Open an autocommit PostgreSQL connection.
87
+
88
+ :param host: hostname or ip.
89
+ :param user: user name.
90
+ :param password: user password.
91
+ :param database: database name.
92
+ :param port: PostgreSQL port.
93
+ :param timeout: connect timeout(s).
94
+ """
95
+ if self._conn is not None and not self._conn.closed:
96
+ return
97
+ prefix = f'postgres://{user}@{host}:{port}/{database}'
98
+ self._logger.extra['prefix'] = prefix
99
+ self._logger.info('Connecting...')
100
+ try:
101
+ self._conn = psycopg.connect(
102
+ host=host,
103
+ user=user,
104
+ password=password,
105
+ dbname=database,
106
+ port=port,
107
+ connect_timeout=timeout,
108
+ autocommit=True,
109
+ row_factory=tuple_row,
110
+ )
111
+ except psycopg.Error as e:
112
+ raise PGSQLConnectError(f'Could not connect to {prefix}: {str(e)}') from e
113
+
114
+ def disconnect(self) -> None:
115
+ """
116
+ Close the connection; repeated calls are harmless.
117
+ """
118
+ if self._conn is not None and not self._conn.closed:
119
+ self._conn.close()
120
+ self._conn = None
121
+
122
+ @contextmanager
123
+ def transaction(self) -> Generator[None, None, None]:
124
+ """
125
+ Commit on success and roll back on exception.
126
+ Nested calls use savepoints automatically.
127
+
128
+ >>> db = PGSQLConnection() # doctest: +SKIP
129
+ >>> db.connect('127.0.0.1', 'postgres', 'secret', 'postgres')
130
+ >>> with db.transaction():
131
+ ... db.exec("INSERT INTO t VALUES (1)") # commit on exit
132
+ >>> with db.transaction():
133
+ ... db.exec("INSERT INTO t VALUES (2)")
134
+ ... raise RuntimeError('rollback') # row 2 is rolled back
135
+ """
136
+ with self._conn.transaction():
137
+ yield
138
+
139
+ def exec(
140
+ self,
141
+ cmd: str,
142
+ expect: PGCodes | None = PGCodes.SUCCESSFUL_COMPLETION
143
+ ) -> PGSQLCommandResult:
144
+ """
145
+ Execute one SQL command and check its SQLSTATE.
146
+
147
+ ``PGCodes`` is a ``str``-based enum with 263 PostgreSQL 14-18
148
+ SQLSTATE codes. Pass ``expect=None`` to accept any outcome
149
+ without raising.
150
+
151
+ :param cmd: SQL command.
152
+ :param expect: expected SQLSTATE code.
153
+ PGCodes.SUCCESSFUL_COMPLETION: expect SQLSTATE is '00000' (default).
154
+ PGCodes.UNDEFINED_TABLE: expect SQLSTATE is '42P01'.
155
+ None: do not check the result.
156
+
157
+ :return: command result (rows, header, rc, cmd).
158
+
159
+ :raises:
160
+ `.PGSQLCommandError` -- if the result is not as expected.
161
+
162
+ >>> db = PGSQLConnection() # doctest: +SKIP
163
+ >>> db.connect('127.0.0.1', 'postgres', 'secret', 'postgres')
164
+ >>> db.exec('SELECT 1') # successful
165
+ >>> db.exec('SELECT * FROM missing') # PGSQLCommandError
166
+ >>> db.exec('SELECT * FROM missing', expect=PGCodes.UNDEFINED_TABLE) # no error
167
+ >>> db.exec('SELECT * FROM missing', expect=None) # no error
168
+ >>> db.exec('SELECT 1', expect=PGCodes.UNDEFINED_TABLE) # PGSQLCommandError
169
+ """
170
+ extra: dict[str, Any] = {'hook': {}}
171
+ self._logger.info(f"Command: '{cmd}', Expect: '{expect}'", extra=extra)
172
+ cause: psycopg.Error | None = None
173
+ try:
174
+ with self._conn.cursor() as cursor:
175
+ cursor.execute(cmd, prepare=True)
176
+ if cursor.description is None:
177
+ rows: list[tuple[Any, ...]] = []
178
+ header: tuple[str, ...] = ()
179
+ else:
180
+ rows = cursor.fetchall()
181
+ header = tuple(
182
+ column.name for column in cursor.description
183
+ )
184
+ result = PGSQLCommandResult(
185
+ rows,
186
+ header=header,
187
+ rc=PGCodes.SUCCESSFUL_COMPLETION.value,
188
+ cmd=cmd,
189
+ )
190
+ except psycopg.Error as e:
191
+ cause = e
192
+ result = PGSQLCommandResult(
193
+ [],
194
+ header=(),
195
+ rc=getattr(e, 'sqlstate', None),
196
+ cmd=cmd,
197
+ )
198
+ extra['hook']['more'] = result
199
+ if expect != None and result.rc != expect.value:
200
+ msg = textwrap.dedent(f"""\
201
+ Expections not met:
202
+ Command: {cmd}
203
+ Excpect: {expect}
204
+ ReturnCode: {result.rc}
205
+ Output:
206
+ {result}
207
+ """)
208
+ raise PGSQLCommandError(
209
+ msg,
210
+ result=result,
211
+ expect=expect,
212
+ ) from cause
213
+ return result
File without changes
@@ -0,0 +1,5 @@
1
+ """
2
+ Package version.
3
+ """
4
+
5
+ __version__: str = '0.1.0'
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: xbot.plugins.pgsql
3
+ Version: 0.1.0
4
+ Summary: PostgreSQL library for xbot.framework
5
+ Author-email: zhaowcheng <zhaowcheng@163.com>
6
+ License-Expression: BSD-2-Clause
7
+ Project-URL: Homepage, https://github.com/zhaowcheng/xbot.plugins.pgsql
8
+ Project-URL: Issues, https://github.com/zhaowcheng/xbot.plugins.pgsql/issues
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: xbot.framework>=0.5.1
19
+ Requires-Dist: psycopg[binary]<4,>=3
20
+ Requires-Dist: cli_helpers
21
+ Provides-Extra: dev
22
+ Requires-Dist: build; extra == "dev"
23
+ Requires-Dist: mypy; extra == "dev"
24
+ Dynamic: license-file
25
+
26
+ <p align="center">
27
+ <br>English | <a href="README.zh.md">中文</a>
28
+ </p>
29
+
30
+ ---
31
+
32
+ # xbot.plugins.pgsql
33
+
34
+ PostgreSQL library for xbot.framework.
35
+
36
+ ## Introduction
37
+
38
+ A synchronous PostgreSQL client library for [xbot.framework](https://pypi.org/project/xbot.framework/), built on Psycopg 3. It provides a thin connection wrapper with SQLSTATE-based expectation checking, psql-style result rendering, and explicit transaction support.
39
+
40
+ Python >= 3.10, PostgreSQL >= 14. For full API documentation, refer to the docstrings in each module.
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install xbot.plugins.pgsql
46
+ ```
47
+
48
+ ## Getting Started
49
+
50
+ ```python
51
+ from xbot.plugins.pgsql import PGCodes, PGSQLConnection
52
+
53
+ db = PGSQLConnection()
54
+ db.connect(
55
+ host="127.0.0.1",
56
+ user="postgres",
57
+ password="secret",
58
+ database="postgres",
59
+ )
60
+
61
+ result = db.exec(
62
+ "SELECT 1::integer AS id, 'alice'::text AS name",
63
+ )
64
+ print(result)
65
+ print(result.header)
66
+ print(result.rc)
67
+ print(result.cmd)
68
+
69
+ missing = db.exec(
70
+ "SELECT * FROM missing_table",
71
+ expect=PGCodes.UNDEFINED_TABLE,
72
+ )
73
+ assert missing.rc == "42P01"
74
+
75
+ with db.transaction():
76
+ db.exec("INSERT INTO accounts(id, balance) VALUES (1, 100)")
77
+
78
+ db.disconnect()
79
+ ```
80
+
81
+ <p align="center">
82
+ <br>中文 | <a href="README.md">English</a>
83
+ </p>
84
+
85
+ ---
86
+
87
+ # xbot.plugins.pgsql
88
+
89
+ xbot.framework 的 PostgreSQL 客户端库。
90
+
91
+ ## 简介
92
+
93
+ 基于 Psycopg 3 的同步 PostgreSQL 客户端库,面向 [xbot.framework](https://pypi.org/project/xbot.framework/)。提供薄封装连接,支持基于 SQLSTATE 的预期检查、psql 风格结果渲染和显式事务。
94
+
95
+ Python >= 3.10,PostgreSQL >= 14。完整 API 文档请参阅各模块的 docstring。
96
+
97
+ ## 安装
98
+
99
+ ```bash
100
+ pip install xbot.plugins.pgsql
101
+ ```
102
+
103
+ ## 入门
104
+
105
+ ```python
106
+ from xbot.plugins.pgsql import PGCodes, PGSQLConnection
107
+
108
+ db = PGSQLConnection()
109
+ db.connect(
110
+ host="127.0.0.1",
111
+ user="postgres",
112
+ password="secret",
113
+ database="postgres",
114
+ )
115
+
116
+ result = db.exec(
117
+ "SELECT 1::integer AS id, 'alice'::text AS name",
118
+ )
119
+ print(result)
120
+ print(result.header)
121
+ print(result.rc)
122
+ print(result.cmd)
123
+
124
+ missing = db.exec(
125
+ "SELECT * FROM missing_table",
126
+ expect=PGCodes.UNDEFINED_TABLE,
127
+ )
128
+ assert missing.rc == "42P01"
129
+
130
+ with db.transaction():
131
+ db.exec("INSERT INTO accounts(id, balance) VALUES (1, 100)")
132
+
133
+ db.disconnect()
134
+ ```
@@ -0,0 +1,14 @@
1
+ tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ tests/run.py,sha256=S7-Nt-cvKStKydBaJptLARRAE7Vs2ng6N-ubsGarhXs,1083
3
+ tests/test_pgsql.py,sha256=3k6r5qb0cNQIjsH8ev1CpVVJ3LFdcw7W65g3d-HL6UA,8947
4
+ xbot/plugins/pgsql/__init__.py,sha256=nSbwueWFzdbcmcENLAsnQiSqEbQ1yzvIG_jxJCrC27g,439
5
+ xbot/plugins/pgsql/codes.py,sha256=5OEIs0YNHbZYGm9jIWczMVuQvu3TTQCn_GD04IAypyg,12400
6
+ xbot/plugins/pgsql/errors.py,sha256=9SRtFCc5ee9r2OMS8SFd8ml8H1LcSlvO8O2WqjLh3WU,1103
7
+ xbot/plugins/pgsql/pgsql.py,sha256=D31ULKlzwHLb2vdGenhPNnmItxpXeIok0ZLeNsQjCig,6877
8
+ xbot/plugins/pgsql/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ xbot/plugins/pgsql/version.py,sha256=yUaeFX3keaqxK7DotElYbBSr-vs68q_f61nlL3WZisw,53
10
+ xbot_plugins_pgsql-0.1.0.dist-info/licenses/LICENSE,sha256=QQgf5JlcyKhtVWQcDwDfv7aU_8RZ5qZKxLnpp3NEXos,1325
11
+ xbot_plugins_pgsql-0.1.0.dist-info/METADATA,sha256=yOpUjIHYUG4J9DrwJ24w_C3ioX3bjj9kFWa5clI4KvQ,3235
12
+ xbot_plugins_pgsql-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
13
+ xbot_plugins_pgsql-0.1.0.dist-info/top_level.txt,sha256=4uDSWEEQ4RLxZEPAyhoF4SDnil_CTMm2yOv0bRpEBNU,11
14
+ xbot_plugins_pgsql-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,24 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2022-2023, zhaowcheng <zhaowcheng@163.com>
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,2 @@
1
+ tests
2
+ xbot