setup.go (6586B)
1 package main 2 3 import ( 4 "bufio" 5 "bytes" 6 "database/sql" 7 "fmt" 8 "html/template" 9 "io" 10 "os" 11 "strings" 12 13 tea "github.com/charmbracelet/bubbletea" 14 "github.com/goccy/go-json" 15 ) 16 17 func setupTables(db *sql.DB) error { 18 _, err := db.Exec("create table IF NOT EXISTS words (word text not null, definition text);") 19 if err != nil { 20 return fmt.Errorf("creating table: %s", err) 21 } 22 23 // Faster import performance. 24 _, err = db.Exec("PRAGMA synchronous = OFF;") 25 if err != nil { 26 return fmt.Errorf("setting risky writes: %s", err) 27 } 28 29 return nil 30 } 31 32 func isDatabaseEmpty(db *sql.DB) tea.Cmd { 33 return func() tea.Msg { 34 row := db.QueryRow(`SELECT count(*) as count from words`) 35 var count int 36 err := row.Scan(&count) 37 if err != nil { 38 return errMsg(fmt.Errorf("counting rows: %s", err)) 39 } 40 41 // Only populate the database if it is empty. 42 return isDictionaryEmptyMsg(count == 0) 43 } 44 } 45 46 type rawDictionaryEntry struct { 47 Word string `json:"word"` 48 LangCode string `json:"lang_code"` 49 POS string `json:"pos_title"` 50 Etymology []string `json:"etymology_texts"` 51 Senses []sense `json:"senses"` 52 Sounds []sound `json:"sounds"` 53 Tags []string `json:"tags"` 54 } 55 56 type sense struct { 57 Glosses []string `json:"glosses"` 58 Examples []example `json:"examples"` 59 } 60 61 type example struct { 62 Text string `json:"text"` 63 } 64 65 type sound struct { 66 IPA string `json:"ipa"` 67 } 68 69 type templateReadyDictionaryEntry struct { 70 Word string 71 POS string 72 Etymology string 73 Senses []SenseForDictionaryEntry 74 Sound string 75 Gender string 76 } 77 78 type SenseForDictionaryEntry struct { 79 Sense string 80 Example string 81 } 82 83 // dictionaryPopulator contains all the information required to populate the 84 // SQLite dictionary from the raw JSONL data. This is in a struct so that we can 85 // report progress back to the UI, then resume where we left off. 86 type dictionaryPopulator struct { 87 db *sql.DB 88 rawDictionaryPath string 89 langCode string 90 91 tx *sql.Tx 92 stmt *sql.Stmt 93 tmpl *template.Template 94 fh *os.File 95 scanner *bufio.Scanner 96 97 totalLines int 98 currentLine int 99 } 100 101 func setupPopulator(dp *dictionaryPopulator) tea.Cmd { 102 return func() tea.Msg { 103 var err error 104 105 // Set up the template 106 dp.tmpl, err = template.New("entry").Parse( 107 `<p>{{ .Word }} {{ .Sound }} <i>{{ .POS }} {{ .Gender }}</i></p> 108 <ol>{{ range .Senses}} 109 <li class=sense>{{ .Sense }}<br> 110 {{ if .Example }}<ul><li><i>{{ .Example }}</i></li></ul></li>{{ end }} 111 {{ end }}</ol> 112 {{ if .Etymology }}<p><i>Étymologie: {{ .Etymology }}</i>{{ end }}`) 113 if err != nil { 114 return errMsg(fmt.Errorf("preparing template: %w", err)) 115 } 116 117 dp.tx, err = dp.db.Begin() 118 if err != nil { 119 return errMsg(fmt.Errorf("starting transaction: %w", err)) 120 } 121 122 // Set up a prepared statement 123 dp.stmt, err = dp.tx.Prepare("insert into words(word, definition) values(?, ?)") 124 if err != nil { 125 return errMsg(fmt.Errorf("preparing statement: %w", err)) 126 } 127 128 dp.fh, err = os.Open(dp.rawDictionaryPath) 129 if err != nil { 130 return errMsg(fmt.Errorf("opening: %w", err)) 131 } 132 133 // Figure out how many lines the file has, for reporting import 134 // progress. 135 lines, err := lineCounter(dp.fh) 136 if err != nil { 137 return errMsg(fmt.Errorf("reading lines from file: %w", err)) 138 } 139 dp.totalLines = lines 140 141 // We've just read through the whole file, reset the read position to 142 // the beginning because we're about to set up a scanner on it. 143 dp.fh.Seek(0, 0) 144 145 dp.scanner = bufio.NewScanner(dp.fh) 146 maxCapacity := 2_000_000 147 buf := make([]byte, maxCapacity) 148 dp.scanner.Buffer(buf, maxCapacity) 149 150 return populatingDictionaryMsg(dp) 151 } 152 } 153 154 func lineCounter(r io.Reader) (int, error) { 155 buf := make([]byte, 64*1024) 156 count := 0 157 lineSep := []byte{'\n'} 158 159 for { 160 c, err := r.Read(buf) 161 count += bytes.Count(buf[:c], lineSep) 162 163 switch { 164 case err == io.EOF: 165 return count, nil 166 167 case err != nil: 168 return count, err 169 } 170 } 171 } 172 173 func populateDictionary(dp *dictionaryPopulator) tea.Cmd { 174 return func() tea.Msg { 175 for dp.scanner.Scan() { 176 dp.currentLine++ 177 178 var result rawDictionaryEntry 179 json.Unmarshal([]byte(dp.scanner.Text()), &result) 180 if result.LangCode != dp.langCode { 181 continue 182 } 183 184 // Clean up the word. Replace apostrophes (common in phrases) with 185 // single quotes (more likely to be typed by a user). 186 result.Word = strings.ReplaceAll(result.Word, `’`, `'`) 187 188 // Create the definition text. 189 entry := templateReadyDictionaryEntry{ 190 Word: result.Word, 191 POS: strings.ToLower(result.POS), 192 } 193 if len(result.Etymology) > 0 { 194 entry.Etymology = strings.TrimSpace(result.Etymology[0]) 195 } 196 if len(result.Sounds) > 0 { 197 entry.Sound = result.Sounds[0].IPA 198 } 199 200 var genders, numbers []string 201 for _, r := range result.Tags { 202 switch r { 203 case "masculine": 204 genders = append(genders, "masculin") 205 case "feminine": 206 genders = append(genders, "féminin") 207 case "plural": 208 numbers = append(numbers, "pluriel") 209 case "singular": 210 numbers = append(numbers, "singulier") 211 } 212 } 213 entry.Gender = strings.Join( 214 []string{ 215 strings.Join(genders, " / "), 216 strings.Join(numbers, " et "), 217 }, 218 " ", 219 ) 220 221 for _, s := range result.Senses { 222 var example string 223 if len(s.Examples) > 0 { 224 example = s.Examples[0].Text 225 } 226 sense := strings.Join(s.Glosses, "; ") 227 entry.Senses = append( 228 entry.Senses, 229 SenseForDictionaryEntry{Sense: sense, Example: example}, 230 ) 231 } 232 233 formattedDefinition := strings.Builder{} 234 err := dp.tmpl.Execute(&formattedDefinition, entry) 235 if err != nil { 236 return errMsg(fmt.Errorf("failed to render: %w", err)) 237 } 238 239 // Insert the entry 240 _, err = dp.stmt.Exec(entry.Word, formattedDefinition.String()) 241 if err != nil { 242 return errMsg(fmt.Errorf("inserting '%s': %w", entry.Word, err)) 243 } 244 245 // Report status every once in a while by breaking out to the caller 246 if dp.currentLine%10000 == 0 { 247 return populatingDictionaryMsg(dp) 248 } 249 } 250 251 // If we're outside of the loop, we either encountered an error, or it's 252 // time to commit the changes. 253 if err := dp.scanner.Err(); err != nil { 254 return errMsg(fmt.Errorf("scanning: %w", err)) 255 } 256 257 if err := dp.tx.Commit(); err != nil { 258 return errMsg(fmt.Errorf("committing: %w", err)) 259 } 260 _, err := dp.db.Exec("create index wordindex on words(word);") 261 if err != nil { 262 return errMsg(fmt.Errorf("creating index: %s", err)) 263 } 264 265 // Clean up resources 266 dp.stmt.Close() 267 dp.fh.Close() 268 269 return isDictionaryEmptyMsg(false) // We're done! 270 } 271 }