sqlsaber 0.27.0__py3-none-any.whl → 0.28.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.
@@ -0,0 +1,225 @@
1
+ """SQL query validation and security using sqlglot AST analysis."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+
6
+ import sqlglot
7
+ from sqlglot import exp
8
+ from sqlglot.errors import ParseError
9
+
10
+ # Prohibited AST node types that indicate write/mutation operations
11
+ # Only include expression types that exist in sqlglot
12
+ PROHIBITED_NODES = {
13
+ # DML operations
14
+ exp.Insert,
15
+ exp.Update,
16
+ exp.Delete,
17
+ exp.Merge,
18
+ # DDL operations
19
+ exp.Create,
20
+ exp.Drop,
21
+ exp.Alter,
22
+ exp.TruncateTable,
23
+ exp.AlterRename,
24
+ # MySQL specific
25
+ exp.Replace,
26
+ # Transaction control
27
+ exp.Transaction,
28
+ # Analysis and maintenance
29
+ exp.Analyze,
30
+ # Data loading/copying
31
+ exp.Copy,
32
+ exp.LoadData,
33
+ # Session and configuration
34
+ exp.Set,
35
+ exp.Use,
36
+ exp.Pragma,
37
+ # Security
38
+ exp.Grant,
39
+ exp.Revoke,
40
+ # Database operations
41
+ exp.Attach,
42
+ exp.Detach,
43
+ # Locking and process control
44
+ exp.Lock,
45
+ exp.Kill,
46
+ # Commands
47
+ exp.Command,
48
+ }
49
+
50
+ try:
51
+ # Add optional types that may not exist in all sqlglot versions
52
+ PROHIBITED_NODES.add(exp.Vacuum)
53
+ except AttributeError:
54
+ pass
55
+
56
+ # Dangerous functions by dialect that can read files or execute commands
57
+ DANGEROUS_FUNCTIONS_BY_DIALECT = {
58
+ "postgres": {
59
+ "pg_read_file",
60
+ "pg_read_binary_file",
61
+ "pg_ls_dir",
62
+ "pg_stat_file",
63
+ "pg_logdir_ls",
64
+ "dblink",
65
+ "dblink_exec",
66
+ },
67
+ "mysql": {
68
+ "load_file",
69
+ "sys_eval",
70
+ "sys_exec",
71
+ },
72
+ "sqlite": {
73
+ "readfile",
74
+ "writefile",
75
+ },
76
+ "tsql": {
77
+ "xp_cmdshell",
78
+ },
79
+ }
80
+
81
+
82
+ @dataclass
83
+ class GuardResult:
84
+ """Result of SQL query validation."""
85
+
86
+ allowed: bool
87
+ reason: Optional[str] = None
88
+ is_select: bool = False
89
+
90
+
91
+ def is_select_like(stmt: exp.Expression) -> bool:
92
+ """Check if statement is a SELECT-like query.
93
+
94
+ Handles CTEs (WITH) and set operations (UNION/INTERSECT/EXCEPT).
95
+ """
96
+ root = stmt
97
+ # WITH wraps another statement
98
+ if isinstance(root, exp.With):
99
+ root = root.this
100
+ return isinstance(root, (exp.Select, exp.Union, exp.Except, exp.Intersect))
101
+
102
+
103
+ def has_prohibited_nodes(stmt: exp.Expression) -> Optional[str]:
104
+ """Walk AST to find any prohibited operations.
105
+
106
+ Checks for:
107
+ - Write operations (INSERT/UPDATE/DELETE/etc)
108
+ - DDL operations (CREATE/DROP/ALTER/etc)
109
+ - SELECT INTO
110
+ - Locking clauses (FOR UPDATE/FOR SHARE)
111
+ """
112
+ for node in stmt.walk():
113
+ # Check prohibited node types
114
+ if isinstance(node, tuple(PROHIBITED_NODES)):
115
+ return f"Prohibited operation: {type(node).__name__}"
116
+
117
+ # Block SELECT INTO (Postgres-style table creation)
118
+ if isinstance(node, exp.Select) and node.args.get("into"):
119
+ return "SELECT INTO is not allowed"
120
+
121
+ # Block locking clauses (FOR UPDATE/FOR SHARE)
122
+ if isinstance(node, exp.Select):
123
+ locks = node.args.get("locks")
124
+ if locks:
125
+ return "SELECT with locking clause (FOR UPDATE/SHARE) is not allowed"
126
+
127
+ return None
128
+
129
+
130
+ def has_dangerous_functions(stmt: exp.Expression, dialect: str) -> Optional[str]:
131
+ """Check for dangerous functions that can read files or execute commands."""
132
+ deny_set = DANGEROUS_FUNCTIONS_BY_DIALECT.get(dialect, set())
133
+ if not deny_set:
134
+ return None
135
+
136
+ deny_lower = {f.lower() for f in deny_set}
137
+
138
+ for fn in stmt.find_all(exp.Func):
139
+ name = fn.name
140
+ if name and name.lower() in deny_lower:
141
+ return f"Use of dangerous function '{name}' is not allowed"
142
+
143
+ return None
144
+
145
+
146
+ def validate_read_only(sql: str, dialect: str = "ansi") -> GuardResult:
147
+ """Validate that SQL query is read-only using AST analysis.
148
+
149
+ Args:
150
+ sql: SQL query to validate
151
+ dialect: SQL dialect (postgres, mysql, sqlite, tsql, etc.)
152
+
153
+ Returns:
154
+ GuardResult with validation outcome
155
+ """
156
+ try:
157
+ statements = sqlglot.parse(sql, read=dialect)
158
+ except ParseError as e:
159
+ return GuardResult(False, f"Unable to parse query safely: {e}")
160
+ except Exception as e:
161
+ return GuardResult(False, f"Error parsing query: {e}")
162
+
163
+ # Only allow single statements
164
+ if len(statements) != 1:
165
+ return GuardResult(
166
+ False,
167
+ f"Only single SELECT statements are allowed (got {len(statements)} statements)",
168
+ )
169
+
170
+ stmt = statements[0]
171
+
172
+ # Must be a SELECT-like statement
173
+ if not is_select_like(stmt):
174
+ return GuardResult(False, "Only SELECT-like statements are allowed")
175
+
176
+ # Check for prohibited operations in the AST
177
+ reason = has_prohibited_nodes(stmt)
178
+ if reason:
179
+ return GuardResult(False, reason)
180
+
181
+ # Check for dangerous functions
182
+ reason = has_dangerous_functions(stmt, dialect)
183
+ if reason:
184
+ return GuardResult(False, reason)
185
+
186
+ return GuardResult(True, None, is_select=True)
187
+
188
+
189
+ def add_limit(sql: str, dialect: str = "ansi", limit: int = 100) -> str:
190
+ """Add LIMIT clause to query if not already present.
191
+
192
+ Args:
193
+ sql: SQL query
194
+ dialect: SQL dialect for proper rendering
195
+ limit: Maximum number of rows to return
196
+
197
+ Returns:
198
+ SQL with LIMIT clause added (or original if LIMIT already exists)
199
+ """
200
+ try:
201
+ statements = sqlglot.parse(sql, read=dialect)
202
+ if len(statements) != 1:
203
+ return sql
204
+
205
+ stmt = statements[0]
206
+
207
+ # Check if LIMIT/TOP/FETCH already exists
208
+ has_limit = any(
209
+ isinstance(n, (exp.Limit, exp.Top, exp.Fetch)) for n in stmt.walk()
210
+ )
211
+ if has_limit:
212
+ return stmt.sql(dialect=dialect)
213
+
214
+ # Add LIMIT - sqlglot will render appropriately for dialect
215
+ # (LIMIT for most, TOP for SQL Server, FETCH FIRST for Oracle)
216
+ stmt = stmt.limit(limit)
217
+ return stmt.sql(dialect=dialect)
218
+
219
+ except Exception:
220
+ # If parsing/transformation fails, fall back to simple string append
221
+ # This maintains backward compatibility
222
+ sql_upper = sql.strip().upper()
223
+ if "LIMIT" not in sql_upper:
224
+ return f"{sql.rstrip(';')} LIMIT {limit};"
225
+ return sql
@@ -9,6 +9,7 @@ from sqlsaber.database.schema import SchemaManager
9
9
  from .base import Tool
10
10
  from .enums import ToolCategory, WorkflowPosition
11
11
  from .registry import register_tool
12
+ from .sql_guard import add_limit, validate_read_only
12
13
 
13
14
 
14
15
  class SQLTool(Tool):
@@ -207,13 +208,17 @@ class ExecuteSQLTool(SQLTool):
207
208
  limit = kwargs.get("limit", self.DEFAULT_LIMIT)
208
209
 
209
210
  try:
210
- # Security check - only allow SELECT queries unless write is enabled
211
- write_error = self._validate_write_operation(query)
212
- if write_error:
213
- return json.dumps({"error": write_error})
211
+ # Get the dialect for this database
212
+ dialect = self.db.sqlglot_dialect
213
+
214
+ # Security check using sqlglot AST analysis
215
+ validation_result = validate_read_only(query, dialect)
216
+ if not validation_result.allowed:
217
+ return json.dumps({"error": validation_result.reason})
214
218
 
215
219
  # Add LIMIT if not present and it's a SELECT query
216
- query = self._add_limit_to_query(query, limit)
220
+ if validation_result.is_select and limit:
221
+ query = add_limit(query, dialect, limit)
217
222
 
218
223
  # Execute the query
219
224
  results = await self.db.execute_query(query)
@@ -249,33 +254,3 @@ class ExecuteSQLTool(SQLTool):
249
254
  )
250
255
 
251
256
  return json.dumps({"error": error_msg, "suggestions": suggestions})
252
-
253
- def _validate_write_operation(self, query: str) -> str | None:
254
- """Validate if a write operation is allowed."""
255
- query_upper = query.strip().upper()
256
-
257
- # Check for write operations
258
- write_keywords = [
259
- "INSERT",
260
- "UPDATE",
261
- "DELETE",
262
- "DROP",
263
- "CREATE",
264
- "ALTER",
265
- "TRUNCATE",
266
- ]
267
- is_write_query = any(query_upper.startswith(kw) for kw in write_keywords)
268
-
269
- if is_write_query:
270
- return (
271
- "Write operations are not allowed. Only SELECT queries are permitted."
272
- )
273
-
274
- return None
275
-
276
- def _add_limit_to_query(self, query: str, limit: int = 100) -> str:
277
- """Add LIMIT clause to SELECT queries if not present."""
278
- query_upper = query.strip().upper()
279
- if query_upper.startswith("SELECT") and "LIMIT" not in query_upper:
280
- return f"{query.rstrip(';')} LIMIT {limit};"
281
- return query
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlsaber
3
- Version: 0.27.0
3
+ Version: 0.28.0
4
4
  Summary: SQLsaber - Open-source agentic SQL assistant
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -17,6 +17,7 @@ Requires-Dist: prompt-toolkit>3.0.51
17
17
  Requires-Dist: pydantic-ai
18
18
  Requires-Dist: questionary>=2.1.0
19
19
  Requires-Dist: rich>=13.7.0
20
+ Requires-Dist: sqlglot[rs]>=27.20.0
20
21
  Description-Content-Type: text/markdown
21
22
 
22
23
  # SQLsaber
@@ -5,44 +5,46 @@ sqlsaber/agents/base.py,sha256=40-MKEoz5rGrqVIylV1U2DaAUSPFcC75ohRin4E3-kk,2668
5
5
  sqlsaber/agents/mcp.py,sha256=Pn8tdDRUEVLYQyEi5nHRp9MKNePwHVVoeNI-uqWcr0Y,757
6
6
  sqlsaber/agents/pydantic_ai_agent.py,sha256=wBxKz0pjOkL-HI-TXV6B67bczZNgu7k26Rr3w5usR3o,10064
7
7
  sqlsaber/application/__init__.py,sha256=KY_-d5nEdQyAwNOsK5r-f7Tb69c63XbuEkHPeLpJal8,84
8
- sqlsaber/application/auth_setup.py,sha256=aIZ4vyfdjO5zCcMy4k4FmqjeGgruypPIRacaOJDuVRY,5080
9
- sqlsaber/application/db_setup.py,sha256=VipoXACDVs8auYs7XeuNYrUWjVHpxqxWphx9yUl1_2I,6826
10
- sqlsaber/application/model_selection.py,sha256=eRWga3vRpoyyTmw0BsVDRvUtyUnxvP8plAQ2Nv7tN3o,3163
8
+ sqlsaber/application/auth_setup.py,sha256=cbS2VTwL7mwm24UFRz84PhC13XMzC1F7JkN-ze7tApY,5104
9
+ sqlsaber/application/db_setup.py,sha256=qtMxCd_KO7GsD4W_iRBpDRvLriiyvOXPvZdcvm6KVDM,6849
10
+ sqlsaber/application/model_selection.py,sha256=xZI-nvUgYZikaTK38SCmEWvWSfRsDpFu2VthbVdI95g,3187
11
11
  sqlsaber/application/prompts.py,sha256=4rMGcWpYJbNWPMzqVWseUMx0nwvXOkWS6GaTAJ5mhfc,3473
12
12
  sqlsaber/cli/__init__.py,sha256=qVSLVJLLJYzoC6aj6y9MFrzZvAwc4_OgxU9DlkQnZ4M,86
13
- sqlsaber/cli/auth.py,sha256=Hz7nuHdX1iMr2UR1VVtu4EAvBYYIhsgthluqpiOXH2A,6064
14
- sqlsaber/cli/commands.py,sha256=R63HF4P0JZwbGlI3TimkEUg3OUaFXYkVvLw1VIy0UFI,8516
13
+ sqlsaber/cli/auth.py,sha256=ysDBXEFR8Jz7wYbIP6X7yWA2ivd8SDnUp_jUg_qYNWk,6088
14
+ sqlsaber/cli/commands.py,sha256=-rTxr-kW7j2rR8wAg0tATKoh284pMDPKVMpQKaJwtqk,8540
15
15
  sqlsaber/cli/completers.py,sha256=g-hLDq5fiBx7gg8Bte1Lq8GU-ZxCYVs4dcPsmHPIcK4,6574
16
- sqlsaber/cli/database.py,sha256=6SdxNuP8w-e2nbV937q4hxfAIwKu1MqoirogD7USaMU,10639
17
- sqlsaber/cli/display.py,sha256=JQBSdLC_a3nkmJ4QfLMhhMPCYY-otS2eXF1XrfEe19E,17045
18
- sqlsaber/cli/interactive.py,sha256=RG4DJN9lprfKi60eomxVt-w3gTsWoqbfEcfO36iibXM,13694
19
- sqlsaber/cli/memory.py,sha256=OufHFJFwV0_GGn7LvKRTJikkWhV1IwNIUDOxFPHXOaQ,7794
20
- sqlsaber/cli/models.py,sha256=v_wrJWV5NXiM3zGLSMyNJf4YhgozK6hnf76Ua8HvysE,8480
21
- sqlsaber/cli/onboarding.py,sha256=5p-rHG2eGM-7t9yg_kNDrXpY13jr__dlFII3K69VtKc,11414
22
- sqlsaber/cli/streaming.py,sha256=YViLCxUv-7WN5TCphLYtAR02HXvuHYuPttGGDZKDUKU,6921
23
- sqlsaber/cli/threads.py,sha256=zVlbOuD3GjjEVNebXwANKeKt4I_Lunf6itiBUL0TaKA,12877
16
+ sqlsaber/cli/database.py,sha256=hh8PdWnhaD0fO2jwvSSQyxsjwk-JyvmcY7f5tuHfnAQ,10663
17
+ sqlsaber/cli/display.py,sha256=WB5JCumhXadziDEX1EZHG3vN1Chol5FNAaTXHieqFK0,17892
18
+ sqlsaber/cli/interactive.py,sha256=u1gvdMZJkFWelZQ2urOS6-EH1uShKF4u_edfr_BzDNk,13479
19
+ sqlsaber/cli/memory.py,sha256=gKP-JJ0w1ya1YTM_Lk7Gw-7wL9ptyj6cZtg-uoW8K7A,7818
20
+ sqlsaber/cli/models.py,sha256=NozZbnisSjbPKo7PW7CltJMIkGcPqTDpDQEY-C_eLhk,8504
21
+ sqlsaber/cli/onboarding.py,sha256=l6FFWn8J1OVQUxr-xIAzKaFhAz8rFh6IEWwIyPWqR6U,11438
22
+ sqlsaber/cli/streaming.py,sha256=1XoZGPPMoTmBQVgp_Bqk483MR93j9oXxSV6Tx_-TpOg,6923
23
+ sqlsaber/cli/threads.py,sha256=5EV4ckRzKqhWeTKpTfQSNCBuqs3onsJURKT09g4E4XM,13634
24
24
  sqlsaber/config/__init__.py,sha256=olwC45k8Nc61yK0WmPUk7XHdbsZH9HuUAbwnmKe3IgA,100
25
- sqlsaber/config/api_keys.py,sha256=7X63B-ow66GZWzYYgc_7f1T26F7gRhfyYQM4RpdkCpI,3647
25
+ sqlsaber/config/api_keys.py,sha256=bjogRmIuxNNGusyKXKi0ZpJWeS5Fyn53zrAD8hsoYx4,3671
26
26
  sqlsaber/config/auth.py,sha256=b5qB2h1doXyO9Bn8z0CcL8LAR2jF431gGXBGKLgTmtQ,2756
27
27
  sqlsaber/config/database.py,sha256=Yec6_0wdzq-ADblMNnbgvouYCimYOY_DWHT9oweaISc,11449
28
- sqlsaber/config/oauth_flow.py,sha256=A3bSXaBLzuAfXV2ZPA94m9NV33c2MyL6M4ii9oEkswQ,10291
29
- sqlsaber/config/oauth_tokens.py,sha256=C9z35hyx-PvSAYdC1LNf3rg9_wsEIY56hkEczelbad0,6015
28
+ sqlsaber/config/oauth_flow.py,sha256=VNbq4TZPk0hVJIcOh7JUO5qSxJnNzqDj0vwjCn-59Ok,10316
29
+ sqlsaber/config/oauth_tokens.py,sha256=SC-lXVcKCV7uiWtBiU2mxvx1z7ryW8tOSKHBApPsXtE,5931
30
30
  sqlsaber/config/providers.py,sha256=JFjeJv1K5Q93zWSlWq3hAvgch1TlgoF0qFa0KJROkKY,2957
31
31
  sqlsaber/config/settings.py,sha256=iB4CnGQ4hw8gxkaa9CVLB_JEy6Y9h9FQTAams5OCVyI,6421
32
32
  sqlsaber/database/__init__.py,sha256=Gi9N_NOkD459WRWXDg3hSuGoBs3xWbMDRBvsTVmnGAg,2025
33
- sqlsaber/database/base.py,sha256=yxYcfeNhRPbO5jFRVZH7eRUGj_up-y3p1ZX_obZXi0w,3552
34
- sqlsaber/database/csv.py,sha256=45eH9mAkBtwSu1Rc_vvG1Z40L4xvfHWSb8OMG15TbCA,4340
35
- sqlsaber/database/duckdb.py,sha256=v6gFUhih5NMbHHpUv7By2nXyl9aqdPtLt0zhqS4-OKE,11120
36
- sqlsaber/database/mysql.py,sha256=5qd9gnSCP3umtBJcQDTzzJfMzwqYCJhWlbOeJZ9_-6c,14349
37
- sqlsaber/database/postgresql.py,sha256=R8I3Y-w0P9qPe47-lmae0X17syIwI8saxEG3etx6Rqc,13097
33
+ sqlsaber/database/base.py,sha256=oaipLxlvoylX6oJCITPAWWqRqv09hRELqqEBufsmFic,3703
34
+ sqlsaber/database/csv.py,sha256=41wuP40FaGPfj28HMiD0I69uG0JbUxArpoTLC3MG2uc,4464
35
+ sqlsaber/database/duckdb.py,sha256=8HNKdx208aFK_YtwGjLz6LTne0xEmNevD-f9dRWlrFg,11244
36
+ sqlsaber/database/mysql.py,sha256=wMzDQqq4GFbfEdqXtv_sCb4Qbr9GSWqYAvOLeo5UryY,14472
37
+ sqlsaber/database/postgresql.py,sha256=fuf2Wl29NKXvD3mqsR08PDleNQ1PG-fNvWSxT6HDh2M,13223
38
38
  sqlsaber/database/resolver.py,sha256=wSCcn__aCqwIfpt_LCjtW2Zgb8RpG5PlmwwZHli1q_U,3628
39
39
  sqlsaber/database/schema.py,sha256=68PrNcA-5eR9PZB3i-TUQw5_E7QatwiDU2wv9GgXgM4,6928
40
- sqlsaber/database/sqlite.py,sha256=zdNj5i4mLJK21sWgftAHDHVihRUWevn__tVF9_nnLfQ,9297
40
+ sqlsaber/database/sqlite.py,sha256=iReEIiSpkhhS1VzITd79ZWqSL3fHMyfe3DRCDpM0DvE,9421
41
41
  sqlsaber/mcp/__init__.py,sha256=COdWq7wauPBp5Ew8tfZItFzbcLDSEkHBJSMhxzy8C9c,112
42
42
  sqlsaber/mcp/mcp.py,sha256=tpNPHpkaCre1Xjp7c4DHXbTKeuYpDQ8qhmJZvAyr7Vk,3939
43
43
  sqlsaber/memory/__init__.py,sha256=GiWkU6f6YYVV0EvvXDmFWe_CxarmDCql05t70MkTEWs,63
44
44
  sqlsaber/memory/manager.py,sha256=p3fybMVfH-E4ApT1ZRZUnQIWSk9dkfUPCyfkmA0HALs,2739
45
45
  sqlsaber/memory/storage.py,sha256=ne8szLlGj5NELheqLnI7zu21V8YS4rtpYGGC7tOmi-s,5745
46
+ sqlsaber/theme/__init__.py,sha256=qCICX1Cg4B6yCbZ1UrerxglWxcqldRFVSRrSs73na_8,188
47
+ sqlsaber/theme/manager.py,sha256=0DWuVXn7JoC8NvAl5FSqc61eagKFTx5YnoY8SoCTxGM,7236
46
48
  sqlsaber/threads/__init__.py,sha256=Hh3dIG1tuC8fXprREUpslCIgPYz8_6o7aRLx4yNeO48,139
47
49
  sqlsaber/threads/storage.py,sha256=rsUdxT4CR52D7xtGir9UlsFnBMk11jZeflzDrk2q4ME,11183
48
50
  sqlsaber/tools/__init__.py,sha256=x3YdmX_7P0Qq_HtZHAgfIVKTLxYqKk6oc4tGsujQWsc,586
@@ -50,9 +52,10 @@ sqlsaber/tools/base.py,sha256=mHhvAj27BHmckyvuDLCPlAQdzABJyYxd9SJnaYAwwuA,1777
50
52
  sqlsaber/tools/enums.py,sha256=CH32mL-0k9ZA18911xLpNtsgpV6tB85TktMj6uqGz54,411
51
53
  sqlsaber/tools/instructions.py,sha256=X-x8maVkkyi16b6Tl0hcAFgjiYceZaSwyWTfmrvx8U8,9024
52
54
  sqlsaber/tools/registry.py,sha256=HWOQMsNIdL4XZS6TeNUyrL-5KoSDH6PHsWd3X66o-18,3211
53
- sqlsaber/tools/sql_tools.py,sha256=2xLD_pkd0t8wKndQAKIr4c9UpWzVWeHbAFpkwo5j4kY,9954
54
- sqlsaber-0.27.0.dist-info/METADATA,sha256=-QcrEttuC3vwUmNdtjHzA_laIfTkcMGoghmSCvNn3nM,7138
55
- sqlsaber-0.27.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
56
- sqlsaber-0.27.0.dist-info/entry_points.txt,sha256=qEbOB7OffXPFgyJc7qEIJlMEX5RN9xdzLmWZa91zCQQ,162
57
- sqlsaber-0.27.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
58
- sqlsaber-0.27.0.dist-info/RECORD,,
55
+ sqlsaber/tools/sql_guard.py,sha256=dTDwcZP-N4xPGzcr7MQtKUxKrlDzlc1irr9aH5a4wvk,6182
56
+ sqlsaber/tools/sql_tools.py,sha256=ujmAcfLkNaBrb5LWEgWcINQEQSX0LRPX3VK5Dag1Sj4,9178
57
+ sqlsaber-0.28.0.dist-info/METADATA,sha256=oY9Awl9jLkdeJLa1oeTPHPGH94KktJ_HmBVUVLQs-do,7174
58
+ sqlsaber-0.28.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
59
+ sqlsaber-0.28.0.dist-info/entry_points.txt,sha256=qEbOB7OffXPFgyJc7qEIJlMEX5RN9xdzLmWZa91zCQQ,162
60
+ sqlsaber-0.28.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
61
+ sqlsaber-0.28.0.dist-info/RECORD,,