hiiro 0.1.308.pre.6 → 0.1.308
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/bin/h-db +98 -0
- data/lib/hiiro/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 293e176ea2c898019b001c0db7277c079980b304b43199dc270d31d91a869cca
|
|
4
|
+
data.tar.gz: c3b6118daf238da019e7f739b2b602d6eab1f6b643c18d3dfb6f502335887e4b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d5878d6857a592da9b566b93eb8df31918196584e58c1f8ee4b637bbf281fbf0c8e72fd207d9751754eadadfbde8124d35c2b1f178bce9426d3809bfffe7494b
|
|
7
|
+
data.tar.gz: 6217a52654077df7be803b923de5a87fb58efb3e0dce4ec51601e4cd040e98a2f911e70755ced173e5b3764de291acece7792259eb97ee1a19b76a3f04e4839a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
```markdown
|
|
2
2
|
# Changelog
|
|
3
3
|
|
|
4
|
+
## [0.1.308] - 2026-03-31
|
|
5
|
+
|
|
6
|
+
### Added
|
|
7
|
+
- `h db cleanup` subcommand to preview and prune duplicate rows from SQLite tables
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- Prevent duplicate pinned_prs during import with `insert_conflict` and per-row rescue
|
|
11
|
+
|
|
4
12
|
## [0.1.308.pre.6] - 2026-03-31
|
|
5
13
|
|
|
6
14
|
### Fixed
|
data/bin/h-db
CHANGED
|
@@ -171,6 +171,104 @@ Hiiro.run(*ARGV) do
|
|
|
171
171
|
Hiiro::DB.remigrate!(only: tables)
|
|
172
172
|
end
|
|
173
173
|
|
|
174
|
+
add_subcmd(:cleanup) do
|
|
175
|
+
require 'fileutils'
|
|
176
|
+
db = Hiiro::DB.connection
|
|
177
|
+
timestamp = Time.now.strftime('%Y%m%d%H%M%S')
|
|
178
|
+
sql_path = File.expand_path("~/notes/files/hiiro-cleanup-#{timestamp}.sql")
|
|
179
|
+
FileUtils.mkdir_p(File.dirname(sql_path))
|
|
180
|
+
|
|
181
|
+
# Natural unique keys per table — used to detect duplicate groups
|
|
182
|
+
dedup_keys = {
|
|
183
|
+
prs: %w[number],
|
|
184
|
+
branches: %w[name],
|
|
185
|
+
tags: %w[name taggable_type taggable_id],
|
|
186
|
+
links: %w[url],
|
|
187
|
+
tasks: %w[name],
|
|
188
|
+
apps: %w[name],
|
|
189
|
+
projects: %w[name],
|
|
190
|
+
assignments: %w[worktree],
|
|
191
|
+
pins: %w[command key],
|
|
192
|
+
pane_homes: %w[name],
|
|
193
|
+
check_runs: %w[pr_number name],
|
|
194
|
+
reminders: %w[text],
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
lines = []
|
|
198
|
+
lines << "-- Hiiro DB duplicate cleanup — #{Time.now.iso8601}"
|
|
199
|
+
lines << "-- Review each statement. Remove lines you do NOT want to run, then:"
|
|
200
|
+
lines << "--"
|
|
201
|
+
lines << "-- sqlite3 #{Hiiro::DB::DB_FILE} < #{sql_path}"
|
|
202
|
+
lines << "--"
|
|
203
|
+
lines << "-- Or run individual statements with:"
|
|
204
|
+
lines << "--"
|
|
205
|
+
lines << "-- h db q \"DELETE FROM ...\""
|
|
206
|
+
lines << "--"
|
|
207
|
+
lines << ""
|
|
208
|
+
|
|
209
|
+
found_any = false
|
|
210
|
+
|
|
211
|
+
dedup_keys.each do |table, keys|
|
|
212
|
+
next unless db.table_exists?(table)
|
|
213
|
+
schema_cols = db.schema(table).map { |col, _| col.to_s }
|
|
214
|
+
next unless (keys - schema_cols).empty?
|
|
215
|
+
next unless schema_cols.include?('id')
|
|
216
|
+
|
|
217
|
+
key_cols_sql = keys.map { |k| "\"#{k}\"" }.join(', ')
|
|
218
|
+
dupes = db.fetch(<<~SQL).all
|
|
219
|
+
SELECT #{key_cols_sql}, COUNT(*) AS cnt
|
|
220
|
+
FROM #{table}
|
|
221
|
+
GROUP BY #{key_cols_sql}
|
|
222
|
+
HAVING cnt > 1
|
|
223
|
+
SQL
|
|
224
|
+
next if dupes.empty?
|
|
225
|
+
|
|
226
|
+
found_any = true
|
|
227
|
+
lines << "-- ===== #{table}: #{dupes.length} duplicate group#{'s' unless dupes.length == 1} ====="
|
|
228
|
+
lines << ""
|
|
229
|
+
|
|
230
|
+
dupes.each do |row|
|
|
231
|
+
# Build a Sequel dataset filtered to this duplicate group
|
|
232
|
+
ds = db[table]
|
|
233
|
+
keys.each do |k|
|
|
234
|
+
val = row[k.to_sym]
|
|
235
|
+
ds = val.nil? ? ds.where(Sequel.lit("\"#{k}\" IS NULL")) : ds.where(k.to_sym => val)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
all_ids = ds.select(:id).map(:id).sort
|
|
239
|
+
keep_id = all_ids.max
|
|
240
|
+
drop_ids = all_ids - [keep_id]
|
|
241
|
+
|
|
242
|
+
key_desc = keys.map { |k| "#{k}=#{row[k.to_sym].inspect}" }.join(', ')
|
|
243
|
+
lines << "-- #{table}: #{key_desc}"
|
|
244
|
+
lines << "-- keeping id=#{keep_id}, dropping id=#{drop_ids.join(', ')}"
|
|
245
|
+
lines << "DELETE FROM #{table} WHERE id IN (#{drop_ids.join(', ')});"
|
|
246
|
+
lines << ""
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
if found_any
|
|
251
|
+
File.write(sql_path, lines.join("\n"))
|
|
252
|
+
|
|
253
|
+
# Update ~/notes/files/INDEX
|
|
254
|
+
index_path = File.expand_path('~/notes/files/INDEX')
|
|
255
|
+
File.open(index_path, 'a') { |f| f.puts sql_path }
|
|
256
|
+
|
|
257
|
+
edit_files(sql_path)
|
|
258
|
+
|
|
259
|
+
puts
|
|
260
|
+
puts "SQL file: #{sql_path}"
|
|
261
|
+
puts
|
|
262
|
+
puts "To apply after editing:"
|
|
263
|
+
puts " sqlite3 #{Hiiro::DB::DB_FILE} < #{sql_path}"
|
|
264
|
+
puts
|
|
265
|
+
puts "Or run individual statements with:"
|
|
266
|
+
puts " h db q \"DELETE FROM ...\""
|
|
267
|
+
else
|
|
268
|
+
puts "No duplicates found across tracked tables."
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
174
272
|
add_subcmd(:restore) do
|
|
175
273
|
config_dir = File.expand_path('~/.config/hiiro')
|
|
176
274
|
archives = Dir.glob(File.join(config_dir, 'sqlite-migration.*.tar.gz')).sort
|
data/lib/hiiro/version.rb
CHANGED