sqlty 0.1.0__tar.gz → 0.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.
Files changed (265) hide show
  1. {sqlty-0.1.0 → sqlty-0.2.0}/PKG-INFO +8 -6
  2. {sqlty-0.1.0 → sqlty-0.2.0}/README.md +7 -5
  3. {sqlty-0.1.0 → sqlty-0.2.0}/pyproject.toml +2 -1
  4. {sqlty-0.1.0 → sqlty-0.2.0}/rust/Cargo.lock +4 -4
  5. {sqlty-0.1.0 → sqlty-0.2.0}/rust/Cargo.toml +1 -1
  6. sqlty-0.2.0/rust/crates/sqlty-ext/build.rs +51 -0
  7. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-ext/src/lib.rs +13 -1
  8. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-python/src/checker.rs +187 -9
  9. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-python/src/fixer.rs +3 -2
  10. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-python/src/rules.rs +11 -2
  11. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-python/src/tokens.rs +13 -0
  12. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/build.rs +3 -1
  13. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/rules/README.md +96 -18
  14. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/rules/common/aggregates.inference.toml +41 -1
  15. sqlty-0.2.0/rust/crates/sqlty-sql/rules/common/conditional.inference.toml +17 -0
  16. sqlty-0.2.0/rust/crates/sqlty-sql/rules/common/datetime.inference.toml +25 -0
  17. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/rules/common/json.inference.toml +34 -2
  18. sqlty-0.2.0/rust/crates/sqlty-sql/rules/common/math.inference.toml +17 -0
  19. sqlty-0.2.0/rust/crates/sqlty-sql/rules/common/strings.inference.toml +33 -0
  20. sqlty-0.2.0/rust/crates/sqlty-sql/rules/common/window.inference.toml +41 -0
  21. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/rules/compile.rs +387 -69
  22. sqlty-0.2.0/rust/crates/sqlty-sql/rules/dialects.toml +6 -0
  23. sqlty-0.2.0/rust/crates/sqlty-sql/rules/mysql/aggregates.inference.toml +49 -0
  24. sqlty-0.2.0/rust/crates/sqlty-sql/rules/mysql/arithmetic.inference.toml +132 -0
  25. sqlty-0.2.0/rust/crates/sqlty-sql/rules/mysql/bits.inference.toml +10 -0
  26. sqlty-0.2.0/rust/crates/sqlty-sql/rules/mysql/comparison.inference.toml +28 -0
  27. sqlty-0.2.0/rust/crates/sqlty-sql/rules/mysql/conditional.inference.toml +19 -0
  28. sqlty-0.2.0/rust/crates/sqlty-sql/rules/mysql/datetime.inference.toml +136 -0
  29. sqlty-0.2.0/rust/crates/sqlty-sql/rules/mysql/encryption.inference.toml +46 -0
  30. sqlty-0.2.0/rust/crates/sqlty-sql/rules/mysql/json.inference.toml +102 -0
  31. sqlty-0.2.0/rust/crates/sqlty-sql/rules/mysql/math.inference.toml +84 -0
  32. sqlty-0.2.0/rust/crates/sqlty-sql/rules/mysql/misc.inference.toml +109 -0
  33. sqlty-0.2.0/rust/crates/sqlty-sql/rules/mysql/spatial.inference.toml +120 -0
  34. sqlty-0.2.0/rust/crates/sqlty-sql/rules/mysql/strings.inference.toml +228 -0
  35. sqlty-0.2.0/rust/crates/sqlty-sql/rules/mysql/system.inference.toml +118 -0
  36. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/admin.inference.toml +145 -0
  37. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/aggregates.inference.toml +178 -0
  38. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/rules/postgres/arithmetic.inference.toml +185 -0
  39. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/arrays.inference.toml +120 -0
  40. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/binary.inference.toml +46 -0
  41. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/comparison.inference.toml +20 -0
  42. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/rules/postgres/concat.inference.toml +20 -0
  43. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/conditional.inference.toml +10 -0
  44. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/datetime.inference.toml +127 -0
  45. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/enums.inference.toml +19 -0
  46. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/events.inference.toml +19 -0
  47. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/extensions.inference.toml +28 -0
  48. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/formatting.inference.toml +28 -0
  49. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/geometry.inference.toml +175 -0
  50. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/json.inference.toml +258 -0
  51. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/math.inference.toml +92 -0
  52. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/merge.inference.toml +10 -0
  53. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/network.inference.toml +64 -0
  54. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/ranges.inference.toml +136 -0
  55. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/rows.inference.toml +10 -0
  56. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/sequences.inference.toml +10 -0
  57. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/series.inference.toml +19 -0
  58. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/strings.inference.toml +101 -0
  59. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/system.inference.toml +353 -0
  60. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/textsearch.inference.toml +162 -0
  61. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/uuid.inference.toml +28 -0
  62. sqlty-0.2.0/rust/crates/sqlty-sql/rules/postgres/xml.inference.toml +55 -0
  63. sqlty-0.2.0/rust/crates/sqlty-sql/rules/sqlite/aggregates.inference.toml +29 -0
  64. sqlty-0.2.0/rust/crates/sqlty-sql/rules/sqlite/arithmetic.inference.toml +54 -0
  65. sqlty-0.2.0/rust/crates/sqlty-sql/rules/sqlite/bitwise.inference.toml +17 -0
  66. sqlty-0.2.0/rust/crates/sqlty-sql/rules/sqlite/datetime.inference.toml +28 -0
  67. sqlty-0.2.0/rust/crates/sqlty-sql/rules/sqlite/fts.inference.toml +37 -0
  68. sqlty-0.2.0/rust/crates/sqlty-sql/rules/sqlite/json.inference.toml +100 -0
  69. sqlty-0.2.0/rust/crates/sqlty-sql/rules/sqlite/math.inference.toml +38 -0
  70. sqlty-0.2.0/rust/crates/sqlty-sql/rules/sqlite/scalar.inference.toml +91 -0
  71. sqlty-0.2.0/rust/crates/sqlty-sql/rules/sqlite/strings.inference.toml +10 -0
  72. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/api.rs +7 -0
  73. sqlty-0.2.0/rust/crates/sqlty-sql/src/analyzer/dml.rs +745 -0
  74. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/explain.rs +6 -1
  75. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/expr.rs +184 -73
  76. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/functions.rs +137 -493
  77. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/grouping.rs +1 -1
  78. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/inference.rs +95 -12
  79. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/json.rs +1 -1
  80. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/literals.rs +7 -1
  81. sqlty-0.2.0/rust/crates/sqlty-sql/src/analyzer/merged_star.rs +178 -0
  82. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/mod.rs +18 -1
  83. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/names.rs +4 -0
  84. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/narrowing.rs +98 -8
  85. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/nullability.rs +404 -115
  86. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/operators.rs +162 -4
  87. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/params.rs +66 -1
  88. sqlty-0.2.0/rust/crates/sqlty-sql/src/analyzer/pg_record_functions.rs +599 -0
  89. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/preprocess.rs +97 -13
  90. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/query.rs +156 -25
  91. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/references.rs +21 -1
  92. sqlty-0.2.0/rust/crates/sqlty-sql/src/analyzer/registry.rs +264 -0
  93. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/resolve.rs +45 -18
  94. sqlty-0.2.0/rust/crates/sqlty-sql/src/analyzer/respell.rs +557 -0
  95. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/scope.rs +85 -17
  96. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__dml__tests__returning_columns_in_queries.snap +11 -10
  97. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__expr__tests__arrays_and_subscripts.snap +1 -1
  98. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__expr__tests__composite_fields_and_json_access.snap +1 -2
  99. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__expr__tests__literals_and_unary_operators.snap +2 -2
  100. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__expr__tests__misc_expressions.snap +1 -1
  101. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__expr__tests__mysql_and_sqlite_expressions.snap +5 -6
  102. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__functions__tests__function_availability_differs_by_dialect.snap +1 -1
  103. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__operators__tests__division_differs_between_postgres_and_mysql.snap +1 -2
  104. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__query__tests__dialects_name_and_type_the_same_query_differently.snap +3 -3
  105. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__query__tests__unsupported_query_forms_are_explained.snap +3 -2
  106. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__query__tests__values_lists.snap +23 -4
  107. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__resolve__tests__quoted_names_keep_their_case_in_postgres_only.snap +1 -1
  108. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__resolve__tests__star_over_unreadable_sources_names_the_reason.snap +8 -4
  109. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__scope__tests__opaque_sources_explain_themselves.snap +5 -5
  110. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__scope__tests__star_expansion_over_joins.snap +15 -6
  111. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/sql_type.rs +5 -4
  112. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/table_functions.rs +103 -32
  113. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/tests.rs +2 -2
  114. sqlty-0.2.0/rust/crates/sqlty-sql/src/analyzer/text_names.rs +174 -0
  115. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/catalog/alter.rs +62 -6
  116. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/catalog/columns.rs +25 -4
  117. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/catalog/loader.rs +208 -20
  118. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/catalog/mod.rs +73 -6
  119. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/catalog/read.rs +157 -8
  120. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/catalog/resolve.rs +25 -7
  121. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/ddl.rs +325 -20
  122. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/diagnostics.rs +6 -1
  123. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/tests/inference_compiler.rs +180 -0
  124. sqlty-0.2.0/skills/sqlty/SKILL.md +81 -0
  125. sqlty-0.2.0/skills/sqlty-adopt/SKILL.md +125 -0
  126. sqlty-0.2.0/skills/sqlty-breaking/SKILL.md +130 -0
  127. sqlty-0.2.0/skills/sqlty-check/SKILL.md +144 -0
  128. sqlty-0.2.0/skills/sqlty-ci/SKILL.md +120 -0
  129. sqlty-0.2.0/skills/sqlty-rewrite/SKILL.md +134 -0
  130. sqlty-0.2.0/src/sqlty/_breaking.py +314 -0
  131. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/_check.py +32 -1
  132. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/_cli.py +69 -24
  133. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/_core.pyi +2 -1
  134. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/_drivers.py +16 -16
  135. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/_helpers.py +17 -1
  136. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/_lint.py +15 -1
  137. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/_params.py +39 -6
  138. sqlty-0.2.0/src/sqlty/_snapshot.py +169 -0
  139. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/_source.py +4 -4
  140. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/_type_extractor/ast_fields.py +55 -9
  141. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/_type_extractor/extract.py +59 -10
  142. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/core.py +34 -29
  143. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/insert-missing-column.md +9 -4
  144. sqlty-0.2.0/src/sqlty/rules/insert-value-count.md +58 -0
  145. sqlty-0.2.0/src/sqlty/rules/narrow-type.md +50 -0
  146. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/param-type-mismatch.md +6 -2
  147. sqlty-0.2.0/src/sqlty/rules/stale-type.md +56 -0
  148. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/unparsed-query.md +2 -2
  149. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/unsupported-syntax.md +3 -3
  150. sqlty-0.1.0/rust/crates/sqlty-sql/rules/mysql/aggregates.inference.toml +0 -21
  151. sqlty-0.1.0/rust/crates/sqlty-sql/rules/mysql/arithmetic.inference.toml +0 -30
  152. sqlty-0.1.0/rust/crates/sqlty-sql/rules/mysql/comparison.inference.toml +0 -10
  153. sqlty-0.1.0/rust/crates/sqlty-sql/rules/mysql/json.inference.toml +0 -21
  154. sqlty-0.1.0/rust/crates/sqlty-sql/rules/mysql/strings.inference.toml +0 -51
  155. sqlty-0.1.0/rust/crates/sqlty-sql/rules/postgres/aggregates.inference.toml +0 -51
  156. sqlty-0.1.0/rust/crates/sqlty-sql/rules/postgres/json.inference.toml +0 -123
  157. sqlty-0.1.0/rust/crates/sqlty-sql/rules/sqlite/json.inference.toml +0 -28
  158. sqlty-0.1.0/rust/crates/sqlty-sql/src/analyzer/dialect_functions.rs +0 -379
  159. sqlty-0.1.0/rust/crates/sqlty-sql/src/analyzer/dml.rs +0 -388
  160. sqlty-0.1.0/rust/crates/sqlty-sql/src/analyzer/registry.rs +0 -439
  161. sqlty-0.1.0/src/sqlty/_breaking.py +0 -191
  162. sqlty-0.1.0/src/sqlty/_widening.py +0 -89
  163. sqlty-0.1.0/src/sqlty/rules/stale-type.md +0 -50
  164. sqlty-0.1.0/src/sqlty/skill/SKILL.md +0 -110
  165. {sqlty-0.1.0 → sqlty-0.2.0}/LICENSE +0 -0
  166. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-ext/Cargo.toml +0 -0
  167. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-python/Cargo.toml +0 -0
  168. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-python/src/edit.rs +0 -0
  169. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-python/src/lib.rs +0 -0
  170. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-python/src/options.rs +0 -0
  171. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-python/src/rewriter.rs +0 -0
  172. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-python/src/scan.rs +0 -0
  173. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-python/src/template.rs +0 -0
  174. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/Cargo.toml +0 -0
  175. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/rules/common/arithmetic.inference.toml +0 -0
  176. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/rules/common/comparison.inference.toml +0 -0
  177. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/rules/common/concat.inference.toml +0 -0
  178. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/rules/mysql/concat.inference.toml +0 -0
  179. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/dialect.rs +0 -0
  180. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/hashing.rs +0 -0
  181. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/memo.rs +0 -0
  182. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__expr__tests__case_expressions.snap +0 -0
  183. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__expr__tests__casts.snap +0 -0
  184. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__expr__tests__casts_without_a_schema_do_not_complain.snap +0 -0
  185. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__functions__tests__aggregates_in_postgres.snap +0 -0
  186. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__functions__tests__return_types_follow_the_argument.snap +0 -0
  187. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__functions__tests__unknown_functions_are_untyped_and_explained.snap +0 -0
  188. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__literals__tests__value_sets_flow_through_expressions.snap +0 -0
  189. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__names__tests__column_names_follow_postgres_figure_colname.snap +0 -0
  190. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__operators__tests__arithmetic_promotes_numbers_and_types_date_math.snap +0 -0
  191. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__operators__tests__binary_operators_in_queries.snap +0 -0
  192. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__operators__tests__common_type_follows_postgres_union_rules.snap +0 -0
  193. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__query__tests__column_names_come_from_the_first_branch.snap +0 -0
  194. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__query__tests__ctes.snap +0 -0
  195. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__query__tests__projection_forms.snap +0 -0
  196. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__query__tests__recursive_ctes_settle_nullability.snap +0 -0
  197. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__query__tests__set_operations_combine_nullability.snap +0 -0
  198. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__query__tests__set_operations_unify_types.snap +0 -0
  199. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__query__tests__statements_that_are_not_queries_have_no_columns.snap +0 -0
  200. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__resolve__tests__ambiguous_names_are_reported_once_per_query.snap +0 -0
  201. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__resolve__tests__cte_columns_are_resolved_like_tables.snap +0 -0
  202. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__resolve__tests__names_are_not_judged_without_a_schema.snap +0 -0
  203. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__resolve__tests__nullability_comes_from_the_declaration.snap +0 -0
  204. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__resolve__tests__system_columns_exist_without_being_declared.snap +0 -0
  205. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__resolve__tests__unknown_names_say_what_is_missing.snap +0 -0
  206. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__resolve__tests__unqualified_names_resolve_to_the_first_source_that_has_them.snap +0 -0
  207. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__scope__tests__correlated_subqueries_see_the_outer_scope.snap +0 -0
  208. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__scope__tests__derived_tables_ctes_and_views.snap +0 -0
  209. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__scope__tests__nullability_follows_the_join_kind.snap +0 -0
  210. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/snapshots/sqlty_sql__analyzer__scope__tests__table_names_fold_by_dialect.snap +0 -0
  211. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/star_expand.rs +0 -0
  212. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/test_support.rs +0 -0
  213. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/analyzer/typed.rs +0 -0
  214. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/catalog/builtin_catalog.sql +0 -0
  215. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/catalog/names.rs +0 -0
  216. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/json.rs +0 -0
  217. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/lib.rs +0 -0
  218. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/placeholders.rs +0 -0
  219. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/src/values.rs +0 -0
  220. {sqlty-0.1.0 → sqlty-0.2.0}/rust/crates/sqlty-sql/tests/timing.rs +0 -0
  221. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/__init__.py +0 -0
  222. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/__main__.py +0 -0
  223. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/_config.py +0 -0
  224. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/_decode.py +0 -0
  225. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/_gitignore.py +0 -0
  226. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/_json_shape.py +0 -0
  227. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/_json_types.py +0 -0
  228. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/_psycopg.py +0 -0
  229. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/_type_extractor/__init__.py +0 -0
  230. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/_type_extractor/compat.py +0 -0
  231. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/_type_extractor/generic.py +0 -0
  232. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/_type_extractor/info.py +0 -0
  233. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/_type_extractor/sandbox.py +0 -0
  234. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/_type_extractor/subtype.py +0 -0
  235. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/drivers/psycopg.py +0 -0
  236. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/magic.py +0 -0
  237. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/py.typed +0 -0
  238. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/ambiguous-column.md +0 -0
  239. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/column-count-mismatch.md +0 -0
  240. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/column-name-mismatch.md +0 -0
  241. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/column-type-mismatch.md +0 -0
  242. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/fstring-skipped.md +0 -0
  243. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/inline-sql-comment.md +0 -0
  244. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/json-shape-mismatch.md +0 -0
  245. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/literal-mismatch.md +0 -0
  246. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/mysql-sql-marker.md +0 -0
  247. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/nullable-mismatch.md +0 -0
  248. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/param-count-mismatch.md +0 -0
  249. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/redundant-literal.md +0 -0
  250. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/redundant-optional.md +0 -0
  251. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/select-star-no-schema.md +0 -0
  252. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/select-star.md +0 -0
  253. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/stale-rewrite.md +0 -0
  254. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/tstring-skipped.md +0 -0
  255. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/typeddict-inheritance.md +0 -0
  256. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/typeddict-key-mismatch.md +0 -0
  257. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/unchecked-query.md +0 -0
  258. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/ungrouped-column.md +0 -0
  259. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/unknown-column.md +0 -0
  260. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/unknown-function.md +0 -0
  261. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/unknown-table.md +0 -0
  262. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/unknown-type.md +0 -0
  263. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/untyped-query.md +0 -0
  264. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty/rules/unused-ignore.md +0 -0
  265. {sqlty-0.1.0 → sqlty-0.2.0}/src/sqlty_magic.pth +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlty
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -111,13 +111,14 @@ CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT);
111
111
  ```python
112
112
  from sqlty import SQL
113
113
 
114
- # error: [stale-type] "SQL[tuple[int, str]] should be SQL[tuple[int, str | None]]"
114
+ # error: [narrow-type] "SQL[tuple[int, str]] should be SQL[tuple[int, str | None]]"
115
115
  query = SQL[tuple[int, str]]("--sql\nSELECT id, email FROM users")
116
116
  ```
117
117
 
118
118
  `sqlty check --fix .` updates the type, and your type checker finds every
119
- caller that now needs a `None` check. `stale-type` is a warning by default; set
120
- `stale-type = "error"` under `[tool.sqlty.rules]` to fail CI on it
119
+ caller that now needs a `None` check. `narrow-type` is an error, so CI fails on
120
+ it; an annotation that is only wider than the rows, such as `str | None` for a
121
+ column made `NOT NULL`, is a `stale-type` warning
121
122
  ([CI](https://jirikuncar.github.io/sqlty/recipes/ci/)). For rolling deploys,
122
123
  `sqlty breaking --against origin/main` exits 1 when old code breaks on the new
123
124
  schema or new code on the old one
@@ -174,8 +175,9 @@ conn.execute("--sql\nSELECT emial FROM users")
174
175
  asyncpg calls ([Parameters](https://jirikuncar.github.io/sqlty/guide/parameters/)).
175
176
  These are the mistakes coding agents make: every problem is one
176
177
  `file:line: [rule] message` line and a non-zero exit code they can act on, and
177
- the [sqlty skill](https://jirikuncar.github.io/sqlty/recipes/agents/) teaches
178
- them the conventions.
178
+ the [sqlty skills](https://jirikuncar.github.io/sqlty/recipes/agents/) teach
179
+ them the conventions. In Claude Code, `/plugin install sqlty --marketplace jirikuncar/sqlty`
180
+ installs them.
179
181
 
180
182
  ## Get started
181
183
 
@@ -78,13 +78,14 @@ CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT);
78
78
  ```python
79
79
  from sqlty import SQL
80
80
 
81
- # error: [stale-type] "SQL[tuple[int, str]] should be SQL[tuple[int, str | None]]"
81
+ # error: [narrow-type] "SQL[tuple[int, str]] should be SQL[tuple[int, str | None]]"
82
82
  query = SQL[tuple[int, str]]("--sql\nSELECT id, email FROM users")
83
83
  ```
84
84
 
85
85
  `sqlty check --fix .` updates the type, and your type checker finds every
86
- caller that now needs a `None` check. `stale-type` is a warning by default; set
87
- `stale-type = "error"` under `[tool.sqlty.rules]` to fail CI on it
86
+ caller that now needs a `None` check. `narrow-type` is an error, so CI fails on
87
+ it; an annotation that is only wider than the rows, such as `str | None` for a
88
+ column made `NOT NULL`, is a `stale-type` warning
88
89
  ([CI](https://jirikuncar.github.io/sqlty/recipes/ci/)). For rolling deploys,
89
90
  `sqlty breaking --against origin/main` exits 1 when old code breaks on the new
90
91
  schema or new code on the old one
@@ -141,8 +142,9 @@ conn.execute("--sql\nSELECT emial FROM users")
141
142
  asyncpg calls ([Parameters](https://jirikuncar.github.io/sqlty/guide/parameters/)).
142
143
  These are the mistakes coding agents make: every problem is one
143
144
  `file:line: [rule] message` line and a non-zero exit code they can act on, and
144
- the [sqlty skill](https://jirikuncar.github.io/sqlty/recipes/agents/) teaches
145
- them the conventions.
145
+ the [sqlty skills](https://jirikuncar.github.io/sqlty/recipes/agents/) teach
146
+ them the conventions. In Claude Code, `/plugin install sqlty --marketplace jirikuncar/sqlty`
147
+ installs them.
146
148
 
147
149
  ## Get started
148
150
 
@@ -61,7 +61,8 @@ manifest-path = "rust/crates/sqlty-ext/Cargo.toml"
61
61
  # registers the lazy "sqlty" codec lookup (see docs/guide/magic-codec.md).
62
62
  # The path is relative to the project root (maturin >= 1.12.1): the wheel gets
63
63
  # it at the root (python-source prefix stripped), the sdist at src/.
64
- include = ["src/sqlty_magic.pth"]
64
+ # The Agent Skills are compiled into the extension, so only the sdist needs them.
65
+ include = ["src/sqlty_magic.pth", { path = "skills/**/*", format = "sdist" }]
65
66
 
66
67
  [dependency-groups]
67
68
  dev = [
@@ -614,7 +614,7 @@ dependencies = [
614
614
 
615
615
  [[package]]
616
616
  name = "sqlty-bench"
617
- version = "0.1.0"
617
+ version = "0.2.0"
618
618
  dependencies = [
619
619
  "criterion",
620
620
  "sqlty-python",
@@ -623,7 +623,7 @@ dependencies = [
623
623
 
624
624
  [[package]]
625
625
  name = "sqlty-core"
626
- version = "0.1.0"
626
+ version = "0.2.0"
627
627
  dependencies = [
628
628
  "pyo3",
629
629
  "sqlty-python",
@@ -633,7 +633,7 @@ dependencies = [
633
633
 
634
634
  [[package]]
635
635
  name = "sqlty-python"
636
- version = "0.1.0"
636
+ version = "0.2.0"
637
637
  dependencies = [
638
638
  "regex",
639
639
  "sqlty-sql",
@@ -642,7 +642,7 @@ dependencies = [
642
642
 
643
643
  [[package]]
644
644
  name = "sqlty-sql"
645
- version = "0.1.0"
645
+ version = "0.2.0"
646
646
  dependencies = [
647
647
  "insta",
648
648
  "regex",
@@ -3,7 +3,7 @@ resolver = "2"
3
3
  members = ["crates/*"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.1.0"
6
+ version = "0.2.0"
7
7
  edition = "2021"
8
8
  description = "Rust engine of sqlty: SQL type inference and Python source rewriting"
9
9
  license = "MIT"
@@ -0,0 +1,51 @@
1
+ //! Embeds the Agent Skills of `skills/` at the repository root, so `sqlty skill` prints the
2
+ //! ones of its own version wherever the wheel is installed.
3
+
4
+ use std::fmt::Write as _;
5
+ use std::path::{Path, PathBuf};
6
+ use std::{env, fs};
7
+
8
+ fn main() {
9
+ let root = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()).join("../../../skills");
10
+ let root = root
11
+ .canonicalize()
12
+ .unwrap_or_else(|e| panic!("{}: {e}", root.display()));
13
+ let mut files = Vec::new();
14
+ collect(&root, &mut files);
15
+ files.sort();
16
+ let mut out = String::from("pub(crate) static SKILLS: &[(&str, &str)] = &[\n");
17
+ for file in &files {
18
+ let relative = file
19
+ .strip_prefix(&root)
20
+ .unwrap()
21
+ .to_str()
22
+ .unwrap()
23
+ .replace('\\', "/");
24
+ writeln!(
25
+ out,
26
+ " ({relative:?}, include_str!({:?})),",
27
+ file.to_str().unwrap()
28
+ )
29
+ .unwrap();
30
+ }
31
+ out.push_str("];\n");
32
+ fs::write(
33
+ Path::new(&env::var("OUT_DIR").unwrap()).join("skills.rs"),
34
+ out,
35
+ )
36
+ .unwrap();
37
+ }
38
+
39
+ /// Every file under `dir`, and every directory for Cargo to watch.
40
+ fn collect(dir: &Path, files: &mut Vec<PathBuf>) {
41
+ println!("cargo:rerun-if-changed={}", dir.display());
42
+ for entry in fs::read_dir(dir).unwrap() {
43
+ let path = entry.unwrap().path();
44
+ if path.is_dir() {
45
+ collect(&path, files);
46
+ } else {
47
+ println!("cargo:rerun-if-changed={}", path.display());
48
+ files.push(path);
49
+ }
50
+ }
51
+ }
@@ -52,6 +52,7 @@ type ParamTuple = (
52
52
  Option<String>,
53
53
  Option<bool>,
54
54
  bool,
55
+ Option<String>,
55
56
  );
56
57
 
57
58
  /// A parsed schema and the tables and views it could not parse.
@@ -296,6 +297,14 @@ fn infer_type(
296
297
  Ok(py.detach(|| analyzer::infer_type(sql, dialect, schema(schema_sql, dialect).as_deref()))?)
297
298
  }
298
299
 
300
+ include!(concat!(env!("OUT_DIR"), "/skills.rs"));
301
+
302
+ /// The files of the Agent Skills, as `(path, text)` with paths like `sqlty/SKILL.md`.
303
+ #[pyfunction]
304
+ fn skills() -> Vec<(&'static str, &'static str)> {
305
+ SKILLS.to_vec()
306
+ }
307
+
299
308
  /// Result columns of a query as `(name, python_type, sql_type)` tuples.
300
309
  ///
301
310
  /// Returns None when the query returns no rows or doesn't parse; unknown types are None.
@@ -453,6 +462,7 @@ fn analyze_params(
453
462
  param.sql_type,
454
463
  nullable(param.nullability),
455
464
  param.cast,
465
+ param.untyped_sql_type,
456
466
  )
457
467
  })
458
468
  .collect(),
@@ -483,7 +493,8 @@ fn name_rules() -> Vec<&'static str> {
483
493
 
484
494
  /// Why a query has untyped columns, or names what it cannot, as `(rule, message)` pairs:
485
495
  /// `unknown-table`, `unknown-column`, `ambiguous-column`, `unknown-function`,
486
- /// `unknown-type`, `unsupported-syntax`, `insert-missing-column` and `ungrouped-column`.
496
+ /// `unknown-type`, `unsupported-syntax`, `insert-missing-column`, `insert-value-count` and
497
+ /// `ungrouped-column`.
487
498
  /// Tables, columns and
488
499
  /// ambiguity are only judged with a schema.
489
500
  ///
@@ -821,6 +832,7 @@ fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {
821
832
  m.add_function(wrap_pyfunction!(check_source, m)?)?;
822
833
  m.add_function(wrap_pyfunction!(wrapped_queries, m)?)?;
823
834
  m.add_function(wrap_pyfunction!(query_sites, m)?)?;
835
+ m.add_function(wrap_pyfunction!(skills, m)?)?;
824
836
  Ok(())
825
837
  }
826
838
 
@@ -410,7 +410,7 @@ impl Checker<'_> {
410
410
 
411
411
  /// `untyped-query`, `stale-type` and the literal rules, against the inferred row type.
412
412
  fn annotation(&mut self, call: &Call, sql_content: &str, line: usize) {
413
- let annotated = &self.scan.source()[call.annotation.clone()];
413
+ let annotated = call.row_type(self.scan.source());
414
414
  let wrapper = if call.template {
415
415
  self.options.style.template_name()
416
416
  } else {
@@ -437,10 +437,28 @@ impl Checker<'_> {
437
437
  return;
438
438
  }
439
439
 
440
+ if is_scalar(annotated) && self.rules.is_enabled(Rule::NarrowType) {
441
+ if let Some(current) = self
442
+ .infer(sql_content)
443
+ .map(|current| self.spelling.respell(current))
444
+ .filter(|current| current.starts_with("tuple["))
445
+ {
446
+ let wrapper = if call.template { "SQLTemplate" } else { "SQL" };
447
+ self.report_fix(
448
+ Rule::NarrowType,
449
+ format!("{wrapper}[{annotated}] should be {wrapper}[{current}]: a row is never a single value"),
450
+ line,
451
+ vec![Edit::new(call.annotation.clone(), current)],
452
+ );
453
+ }
454
+ return;
455
+ }
440
456
  let literals = self.rules.is_enabled(Rule::LiteralMismatch)
441
457
  || self.rules.is_enabled(Rule::RedundantLiteral);
442
458
  if !(annotated.starts_with("tuple[")
443
- && (self.rules.is_enabled(Rule::StaleType) || literals))
459
+ && (self.rules.is_enabled(Rule::StaleType)
460
+ || self.rules.is_enabled(Rule::NarrowType)
461
+ || literals))
444
462
  {
445
463
  return;
446
464
  }
@@ -448,9 +466,14 @@ impl Checker<'_> {
448
466
  .infer(sql_content)
449
467
  .map(|current| self.spelling.respell(current))
450
468
  {
451
- if !refines_tuple(annotated, &current) {
469
+ let spelled = spell_unions(annotated);
470
+ if !refines_tuple(&spelled, &current) {
452
471
  self.report_fix(
453
- Rule::StaleType,
472
+ if admits_tuple(&spelled, &current) {
473
+ Rule::StaleType
474
+ } else {
475
+ Rule::NarrowType
476
+ },
454
477
  format!(
455
478
  "{}[{annotated}] should be {}[{current}]",
456
479
  if call.template { "SQLTemplate" } else { "SQL" },
@@ -460,7 +483,7 @@ impl Checker<'_> {
460
483
  vec![Edit::new(call.annotation.clone(), current.clone())],
461
484
  );
462
485
  }
463
- for (rule, message) in literal_findings(annotated, &current) {
486
+ for (rule, message) in literal_findings(&spelled, &current) {
464
487
  self.report(rule, message, line);
465
488
  }
466
489
  }
@@ -683,13 +706,39 @@ pub fn wrapped_queries(source: &str, style: &ImportStyle) -> Vec<(usize, String,
683
706
  });
684
707
  (
685
708
  scan.lines.line_at(call.span.start),
686
- source[call.annotation.clone()].to_string(),
709
+ call.row_type(source).to_string(),
687
710
  query,
688
711
  )
689
712
  })
690
713
  .collect()
691
714
  }
692
715
 
716
+ /// Whether `annotated` is a builtin scalar or collection, such as `int` or `list[str]`:
717
+ /// a driver returns each row as a tuple, a dict or an instance of a row class.
718
+ fn is_scalar(annotated: &str) -> bool {
719
+ let members = split_top(annotated, '|');
720
+ members.iter().any(|member| member.trim() != "None")
721
+ && members.iter().all(|member| {
722
+ let base = member.trim().split('[').next().unwrap_or_default();
723
+ matches!(
724
+ base.strip_prefix("typing.").unwrap_or(base),
725
+ "None"
726
+ | "int"
727
+ | "str"
728
+ | "float"
729
+ | "bool"
730
+ | "bytes"
731
+ | "complex"
732
+ | "list"
733
+ | "set"
734
+ | "frozenset"
735
+ | "List"
736
+ | "Set"
737
+ | "FrozenSet"
738
+ )
739
+ })
740
+ }
741
+
693
742
  /// The items of a `tuple[...]` type, split at its top-level commas.
694
743
  fn tuple_items(ty: &str) -> Option<Vec<&str>> {
695
744
  let inner = ty.strip_prefix("tuple[")?.strip_suffix(']')?;
@@ -755,6 +804,69 @@ fn split_top(text: &str, sep: char) -> Vec<&str> {
755
804
  parts
756
805
  }
757
806
 
807
+ /// The members of a tuple item, with `Optional[X]`, `Union[A, B]` and a quoted annotation
808
+ /// spelled out: `Optional[str]` and `"str | None"` are `["str", "None"]`.
809
+ fn union_members(item: &str) -> Vec<&str> {
810
+ let parts = split_top(item, '|');
811
+ if parts.len() > 1 {
812
+ return parts.into_iter().flat_map(union_members).collect();
813
+ }
814
+ let item = item.trim();
815
+ let quoted = ['"', '\''].iter().find_map(|&quote| {
816
+ item.strip_prefix(quote)
817
+ .and_then(|rest| rest.strip_suffix(quote))
818
+ });
819
+ if let Some(inner) = quoted {
820
+ return union_members(inner);
821
+ }
822
+ let bare = item.strip_prefix("typing.").unwrap_or(item);
823
+ if let Some(inner) = bare
824
+ .strip_prefix("Optional[")
825
+ .and_then(|rest| rest.strip_suffix(']'))
826
+ {
827
+ let mut members = union_members(inner);
828
+ members.push("None");
829
+ return members;
830
+ }
831
+ if let Some(inner) = bare
832
+ .strip_prefix("Union[")
833
+ .and_then(|rest| rest.strip_suffix(']'))
834
+ {
835
+ return split_top(inner, ',')
836
+ .into_iter()
837
+ .flat_map(union_members)
838
+ .collect();
839
+ }
840
+ vec![item]
841
+ }
842
+
843
+ /// `annotated` with each `Optional[X]`, `Union[A, B]` or quoted item spelled `A | B | None`,
844
+ /// as sqlty writes it.
845
+ fn spell_unions(annotated: &str) -> String {
846
+ let Some(items) = tuple_items(annotated) else {
847
+ return annotated.to_string();
848
+ };
849
+ let items: Vec<String> = items
850
+ .iter()
851
+ .map(|item| {
852
+ let members = union_members(item);
853
+ let mut spelled: Vec<&str> = members.iter().copied().filter(|m| *m != "None").collect();
854
+ if spelled.len() < members.len() {
855
+ spelled.push("None");
856
+ }
857
+ spelled.join(" | ")
858
+ })
859
+ .collect();
860
+ format!("tuple[{}]", items.join(", "))
861
+ }
862
+
863
+ /// Whether two tuple items are the same union, in any order and spelling.
864
+ fn same_union(annotated: &str, inferred: &str) -> bool {
865
+ let (ours, theirs) = (union_members(annotated), union_members(inferred));
866
+ ours.iter().all(|member| theirs.contains(member))
867
+ && theirs.iter().all(|member| ours.contains(member))
868
+ }
869
+
758
870
  fn literal_member(text: &str) -> Option<Lit> {
759
871
  let Some(quote) = text.chars().next().filter(|c| matches!(c, '\'' | '"')) else {
760
872
  return text.parse().ok().map(Lit::Int);
@@ -929,6 +1041,7 @@ pub(crate) fn refines_tuple(annotated: &str, inferred: &str) -> bool {
929
1041
  ours.iter().zip(&theirs).all(|(a, i)| {
930
1042
  let base = i.split('[').next().unwrap_or(i);
931
1043
  a == i
1044
+ || same_union(a, i)
932
1045
  || covers_literal(a, i)
933
1046
  || literal_pair(a, i).is_some_and(|pair| pair.same_nullability)
934
1047
  || *i == "typing.Any"
@@ -943,6 +1056,45 @@ pub(crate) fn refines_tuple(annotated: &str, inferred: &str) -> bool {
943
1056
  }
944
1057
  }
945
1058
 
1059
+ /// Whether every row of the inferred `tuple[...]` is a row of the annotated one: `str | None`
1060
+ /// admits `str`, `str` admits `Literal['a']`, and `object` or `typing.Any` admit anything.
1061
+ fn admits_tuple(annotated: &str, inferred: &str) -> bool {
1062
+ fn literal_class(member: &str) -> Option<&'static str> {
1063
+ let values = member
1064
+ .strip_prefix("typing.")
1065
+ .unwrap_or(member)
1066
+ .strip_prefix("Literal[")?
1067
+ .strip_suffix(']')?;
1068
+ let values = split_top(values, ',');
1069
+ if values.iter().all(|v| v.starts_with(['\'', '"'])) {
1070
+ Some("str")
1071
+ } else if values.iter().all(|v| matches!(*v, "True" | "False")) {
1072
+ Some("bool")
1073
+ } else if values.iter().all(|v| v.parse::<i64>().is_ok()) {
1074
+ Some("int")
1075
+ } else {
1076
+ None
1077
+ }
1078
+ }
1079
+ fn admits(annotated: &str, inferred: &str) -> bool {
1080
+ let members = union_members(annotated);
1081
+ members
1082
+ .iter()
1083
+ .any(|m| matches!(*m, "typing.Any" | "Any" | "object"))
1084
+ || split_top(inferred, '|').iter().all(|member| {
1085
+ members.contains(member)
1086
+ || literal_class(member).is_some_and(|class| members.contains(&class))
1087
+ })
1088
+ }
1089
+ match (tuple_items(annotated), tuple_items(inferred)) {
1090
+ (Some(ours), Some(theirs)) => match ours.as_slice() {
1091
+ [each, "..."] => theirs.iter().all(|i| admits(each, i)),
1092
+ _ => ours.len() == theirs.len() && ours.iter().zip(&theirs).all(|(a, i)| admits(a, i)),
1093
+ },
1094
+ _ => false,
1095
+ }
1096
+ }
1097
+
946
1098
  #[cfg(test)]
947
1099
  mod tests {
948
1100
  use super::*;
@@ -1094,9 +1246,35 @@ mod tests {
1094
1246
  false,
1095
1247
  &[],
1096
1248
  );
1097
- let stale_types: Vec<_> = diags.iter().filter(|d| d.rule == Rule::StaleType).collect();
1098
- assert_eq!(stale_types.len(), 1);
1099
- assert!(stale_types[0].message.contains("tuple[int]"));
1249
+ let narrow: Vec<_> = diags
1250
+ .iter()
1251
+ .filter(|d| d.rule == Rule::NarrowType)
1252
+ .collect();
1253
+ assert_eq!(narrow.len(), 1);
1254
+ assert!(narrow[0].message.contains("tuple[int]"));
1255
+ }
1256
+
1257
+ #[test]
1258
+ fn test_wider_annotation_is_stale_not_narrow() {
1259
+ for (annotated, inferred, admitted) in [
1260
+ ("tuple[str | None]", "tuple[str]", true),
1261
+ ("tuple[object, typing.Any]", "tuple[int | None, str]", true),
1262
+ (
1263
+ "tuple[str, int, bool]",
1264
+ "tuple[Literal['a', \"b\"], typing.Literal[-1, 2], Literal[True]]",
1265
+ true,
1266
+ ),
1267
+ ("tuple[str]", "tuple[str | None]", false),
1268
+ ("tuple[int]", "tuple[int, str]", false),
1269
+ ("tuple[int]", "tuple[Literal['a']]", false),
1270
+ ("tuple[int]", "dict[str, int]", false),
1271
+ ] {
1272
+ assert_eq!(
1273
+ admits_tuple(annotated, inferred),
1274
+ admitted,
1275
+ "{annotated} {inferred}"
1276
+ );
1277
+ }
1100
1278
  }
1101
1279
 
1102
1280
  #[test]
@@ -19,12 +19,13 @@ use crate::rules::{Rule, RuleConfig};
19
19
  use crate::scan::Scan;
20
20
 
21
21
  /// The rules whose diagnostics carry a fix.
22
- const FIXABLE: [Rule; 6] = [
22
+ const FIXABLE: [Rule; 7] = [
23
23
  Rule::InlineSqlComment,
24
24
  Rule::MysqlSqlMarker,
25
25
  Rule::StaleRewrite,
26
26
  Rule::SelectStar,
27
27
  Rule::StaleType,
28
+ Rule::NarrowType,
28
29
  Rule::UntypedQuery,
29
30
  ];
30
31
 
@@ -78,7 +79,7 @@ pub fn fix_source(
78
79
  for edit in applied {
79
80
  match rule_at[&(edit.range.start, edit.range.end)] {
80
81
  Rule::StaleRewrite => wrapped.push(edit.replacement),
81
- Rule::StaleType | Rule::UntypedQuery => {
82
+ Rule::StaleType | Rule::NarrowType | Rule::UntypedQuery => {
82
83
  introduced.insert(edit.replacement);
83
84
  }
84
85
  _ => {}
@@ -38,8 +38,10 @@ pub enum Rule {
38
38
  SelectStarNoSchema,
39
39
  /// A `--sql` string that is not wrapped in `SQL[T](...)`.
40
40
  StaleRewrite,
41
- /// A `SQL[tuple[...]]` that neither matches nor refines the inferred row type.
41
+ /// A `SQL[tuple[...]]` wider than the inferred row type: it admits every row.
42
42
  StaleType,
43
+ /// A `SQL[tuple[...]]` that does not admit every row the query returns.
44
+ NarrowType,
43
45
  /// A `--sql` marker under MySQL, which rejects it.
44
46
  MysqlSqlMarker,
45
47
  /// `"--sql SELECT ..."`: the whole query is a SQL comment.
@@ -66,7 +68,7 @@ pub enum Rule {
66
68
 
67
69
  impl Rule {
68
70
  /// Every rule.
69
- pub const ALL: [Rule; 32] = [
71
+ pub const ALL: [Rule; 34] = [
70
72
  Rule::ColumnCountMismatch,
71
73
  Rule::ColumnTypeMismatch,
72
74
  Rule::ColumnNameMismatch,
@@ -81,6 +83,7 @@ impl Rule {
81
83
  Rule::SelectStarNoSchema,
82
84
  Rule::StaleRewrite,
83
85
  Rule::StaleType,
86
+ Rule::NarrowType,
84
87
  Rule::MysqlSqlMarker,
85
88
  Rule::InlineSqlComment,
86
89
  Rule::FstringSkipped,
@@ -98,6 +101,7 @@ impl Rule {
98
101
  Rule::Name(NameRule::UnknownType),
99
102
  Rule::Name(NameRule::UnsupportedSyntax),
100
103
  Rule::Name(NameRule::InsertMissingColumn),
104
+ Rule::Name(NameRule::InsertValueCount),
101
105
  Rule::Name(NameRule::UngroupedColumn),
102
106
  ];
103
107
 
@@ -118,6 +122,7 @@ impl Rule {
118
122
  Rule::SelectStarNoSchema => "select-star-no-schema",
119
123
  Rule::StaleRewrite => "stale-rewrite",
120
124
  Rule::StaleType => "stale-type",
125
+ Rule::NarrowType => "narrow-type",
121
126
  Rule::MysqlSqlMarker => "mysql-sql-marker",
122
127
  Rule::InlineSqlComment => "inline-sql-comment",
123
128
  Rule::FstringSkipped => "fstring-skipped",
@@ -149,12 +154,14 @@ impl Rule {
149
154
  | Rule::LiteralMismatch
150
155
  | Rule::ParamCountMismatch
151
156
  | Rule::ParamTypeMismatch
157
+ | Rule::NarrowType
152
158
  | Rule::SelectStarNoSchema
153
159
  | Rule::Name(
154
160
  NameRule::UnknownTable
155
161
  | NameRule::UnknownColumn
156
162
  | NameRule::AmbiguousColumn
157
163
  | NameRule::InsertMissingColumn
164
+ | NameRule::InsertValueCount
158
165
  | NameRule::UngroupedColumn,
159
166
  ) => Severity::Error,
160
167
  Rule::ColumnNameMismatch
@@ -310,6 +317,8 @@ mod tests {
310
317
  let config = RuleConfig::default();
311
318
  assert_eq!(config.severity(Rule::SelectStarNoSchema), Severity::Error);
312
319
  assert_eq!(config.severity(Rule::StaleRewrite), Severity::Warn);
320
+ assert_eq!(config.severity(Rule::StaleType), Severity::Warn);
321
+ assert_eq!(config.severity(Rule::NarrowType), Severity::Error);
313
322
  assert_eq!(config.severity(Rule::UnparsedQuery), Severity::Warn);
314
323
  assert_eq!(config.severity(Rule::RedundantOptional), Severity::Ignore);
315
324
  assert_eq!(
@@ -382,6 +382,19 @@ impl Call {
382
382
  any_calls.binary_search(&self.span.start).is_ok()
383
383
  }
384
384
 
385
+ /// `T` as a type: the content of a string literal (a forward reference, `SQL["User"]`),
386
+ /// else its source.
387
+ pub(crate) fn row_type<'a>(&self, source: &'a str) -> &'a str {
388
+ let written = source[self.annotation.clone()].trim();
389
+ ['"', '\'']
390
+ .into_iter()
391
+ .find_map(|quote| {
392
+ let inner = written.strip_prefix(quote)?.strip_suffix(quote)?;
393
+ (!inner.contains([quote, '\\'])).then_some(inner.trim())
394
+ })
395
+ .unwrap_or(written)
396
+ }
397
+
385
398
  /// The source of the argument's literals, from the first to the last.
386
399
  pub(crate) fn argument<'a>(&self, source: &'a str) -> &'a str {
387
400
  &source[self.strings[0].start..self.strings[self.strings.len() - 1].end]
@@ -38,7 +38,9 @@ fn collect(dir: &Path, files: &mut Vec<PathBuf>) {
38
38
  let path = entry.path();
39
39
  if path.is_dir() {
40
40
  collect(&path, files);
41
- } else if path.to_string_lossy().ends_with(".inference.toml") {
41
+ } else if path.to_string_lossy().ends_with(".inference.toml")
42
+ || path.ends_with("rules/dialects.toml")
43
+ {
42
44
  files.push(path);
43
45
  }
44
46
  }