1fd3b8696f7ad1747301f3af60ecac99a750d5a4
[platform/upstream/ibus-libpinyin.git] / src / PYDatabase.cc
1 /* vim:set et ts=4 sts=4:
2  *
3  * ibus-pinyin - The Chinese PinYin engine for IBus
4  *
5  * Copyright (c) 2008-2010 Peng Huang <shawn.p.huang@gmail.com>
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2, or (at your option)
10  * any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20  */
21 #include "PYDatabase.h"
22 #include <sqlite3.h>
23 #include "PYUtil.h"
24 #include "PYPinyinArray.h"
25
26 namespace PY {
27
28 #define DB_CACHE_SIZE       "5000"
29 #define DB_INDEX_SIZE       (3)
30 /* define columns */
31 #define DB_COLUMN_USER_FREQ (0)
32 #define DB_COLUMN_PHRASE    (1)
33 #define DB_COLUMN_FREQ      (2)
34 #define DB_COLUMN_S0        (3)
35
36 #define DB_PREFETCH_LEN     (6)
37
38 boost::scoped_ptr<Database> Database::m_instance;
39
40 class Conditions : public std::vector<std::string> {
41 public:
42     Conditions (void) : std::vector<std::string> (1) {}
43
44     void double_ (void) {
45         gint i = size ();
46         do {
47             push_back (at (--i));
48         } while (i > 0);
49     }
50
51     void triple (void) {
52         gint i = size ();
53         do {
54             const std::string & value = std::vector<std::string>::at (--i);
55             push_back (value);
56             push_back (value);
57         } while (i > 0);
58     }
59
60     void appendVPrintf (gint begin, gint end, const gchar *fmt, va_list args) {
61         gchar str[64];
62         g_vsnprintf (str, sizeof(str), fmt, args);
63         for (gint i = begin; i < end; i++) {
64             at (i) += str;
65         }
66     }
67
68     void appendPrintf (gint begin, gint end, const gchar *fmt, ...) {
69         va_list args;
70         va_start (args, fmt);
71         appendVPrintf (begin, end, fmt, args);
72         va_end (args);
73     }
74 };
75
76 class SQLStmt {
77 public:
78     SQLStmt (sqlite3 *db)
79         : m_db (db), m_stmt (NULL) {
80         g_assert (m_db != NULL);
81     }
82
83     ~SQLStmt () {
84         if (m_stmt != NULL) {
85             if (sqlite3_finalize (m_stmt) != SQLITE_OK) {
86                 g_warning ("destroy sqlite stmt failed!");
87             }
88         }
89     }
90
91     gboolean prepare (const String &sql) {
92         if (sqlite3_prepare (m_db,
93                              sql.c_str (),
94                              sql.size (),
95                              &m_stmt,
96                              NULL) != SQLITE_OK) {
97             g_warning ("parse sql failed!\n %s", sql.c_str ());
98             return FALSE;
99         }
100
101         return TRUE;
102     }
103
104     gboolean step (void) {
105         switch (sqlite3_step (m_stmt)) {
106         case SQLITE_ROW:
107             return TRUE;
108         case SQLITE_DONE:
109             return FALSE;
110         default:
111             g_warning ("sqlites step error!");
112             return FALSE;
113         }
114     }
115
116     const gchar *columnText (guint col) {
117         return (const gchar *) sqlite3_column_text (m_stmt, col);
118     }
119
120     gint columnInt (guint col) {
121         return sqlite3_column_int (m_stmt, col);
122     }
123
124 private:
125     sqlite3 *m_db;
126     sqlite3_stmt *m_stmt;
127 };
128
129 Query::Query (const PinyinArray    & pinyin,
130               guint                  pinyin_begin,
131               guint                  pinyin_len,
132               guint                  option)
133     : m_pinyin (pinyin),
134       m_pinyin_begin (pinyin_begin),
135       m_pinyin_len (pinyin_len),
136       m_option (option)
137 {
138     g_assert (m_pinyin.size () >= pinyin_begin + pinyin_len);
139 }
140
141 Query::~Query (void)
142 {
143 }
144
145 gint
146 Query::fill (PhraseArray &phrases, gint count)
147 {
148     gint row = 0;
149
150     while (m_pinyin_len > 0) {
151         if (G_LIKELY (m_stmt.get () == NULL)) {
152             m_stmt = Database::instance ().query (m_pinyin, m_pinyin_begin, m_pinyin_len, -1, m_option);
153             g_assert (m_stmt.get () != NULL);
154         }
155
156         while (m_stmt->step ()) {
157             Phrase phrase;
158
159             g_strlcpy (phrase.phrase,
160                        m_stmt->columnText (DB_COLUMN_PHRASE),
161                        sizeof (phrase.phrase));
162             phrase.freq = m_stmt->columnInt (DB_COLUMN_FREQ);
163             phrase.user_freq = m_stmt->columnInt (DB_COLUMN_USER_FREQ);
164             phrase.len = m_pinyin_len;
165
166             for (guint i = 0, column = DB_COLUMN_S0; i < m_pinyin_len; i++) {
167                 phrase.pinyin_id[i].sheng = m_stmt->columnInt (column++);
168                 phrase.pinyin_id[i].yun = m_stmt->columnInt (column++);
169             }
170
171             phrases.push_back (phrase);
172             row ++;
173             if (G_UNLIKELY (row == count)) {
174                 return row;
175             }
176         }
177
178         m_stmt.reset ();
179         m_pinyin_len --;
180     }
181
182     return row;
183 }
184
185 Database::Database (void)
186     : m_db (NULL)
187 {
188     open ();
189 }
190
191 Database::~Database (void)
192 {
193     if (m_db) {
194         if (sqlite3_close (m_db) != SQLITE_OK) {
195             g_warning ("close sqlite database failed!");
196         }
197     }
198 }
199
200 inline gboolean
201 Database::executeSQL (const gchar *sql)
202 {
203     gchar *errmsg;
204     if (sqlite3_exec (m_db, sql, NULL, NULL, &errmsg) != SQLITE_OK) {
205         g_warning ("%s: %s", errmsg, sql);
206         sqlite3_free (errmsg);
207         return FALSE;
208     }
209     return TRUE;
210 }
211
212 gboolean
213 Database::open (void)
214 {
215     gboolean retval;
216
217 #if (SQLITE_VERSION_NUMBER >= 3006000)
218     sqlite3_initialize ();
219 #endif
220
221     static const gchar * maindb [] = {
222         PKGDATADIR"/db/local.db",
223         PKGDATADIR"/db/open-phrase.db",
224         PKGDATADIR"/db/android.db",
225         "main.db",
226     };
227
228     guint i;
229     for (i = 0; i < G_N_ELEMENTS (maindb); i++) {
230         if (!g_file_test(maindb[i], G_FILE_TEST_IS_REGULAR))
231             continue;
232         if (sqlite3_open_v2 (maindb[i], &m_db,
233             SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL) == SQLITE_OK) {
234             g_message ("Use database %s", maindb[i]);
235             break;
236         }
237     }
238
239     if (i == G_N_ELEMENTS (maindb)) {
240         g_warning ("can not open main database");
241         goto _failed;
242     }
243
244     m_sql.clear ();
245
246 #if 1
247     /* Set synchronous=OFF, write user database will become much faster.
248      * It will cause user database corrupted, if the operatering system
249      * crashes or computer loses power.
250      * */
251     m_sql << "PRAGMA synchronous=NORMAL;\n";
252 #endif
253
254     /* Set the cache size for better performance */
255     m_sql << "PRAGMA cache_size=" DB_CACHE_SIZE ";\n";
256
257     /* Using memory for temp store */
258     m_sql << "PRAGMA temp_store=MEMORY;\n";
259
260     /* Set journal mode */
261     m_sql << "PRAGMA journal_mode=PERSIST;\n";
262
263     /* Using EXCLUSIVE locking mode on databases
264      * for better performance */
265     m_sql << "PRAGMA locking_mode=EXCLUSIVE;\n";
266     if (!executeSQL (m_sql))
267         goto _failed;
268
269     /* Attach user database */
270     m_buffer = g_get_user_cache_dir ();
271     m_buffer << G_DIR_SEPARATOR_S << "ibus"
272              << G_DIR_SEPARATOR_S << "pinyin";
273     g_mkdir_with_parents (m_buffer, 0750);
274     m_buffer << G_DIR_SEPARATOR_S << "user-1.3.db";
275     retval = openUserDB (m_buffer);
276     if (!retval) {
277         g_warning ("Can not open user database %s", m_buffer.c_str ());
278         if (!openUserDB (":memory:"))
279             goto _failed;
280     }
281
282     /* prefetch some tables */
283     // prefetch ();
284
285     return TRUE;
286
287 _failed:
288     if (m_db) {
289         sqlite3_close (m_db);
290         m_db = NULL;
291     }
292     return FALSE;
293 }
294
295 gboolean
296 Database::openUserDB (const gchar *userdb)
297 {
298     m_sql.printf ("ATTACH DATABASE \"%s\" AS userdb;", userdb);
299     if (!executeSQL (m_sql))
300         return FALSE;
301
302     m_sql = "BEGIN TRANSACTION;\n";
303     /* create desc table*/
304     m_sql << "CREATE TABLE IF NOT EXISTS userdb.desc (name PRIMARY KEY, value TEXT);\n";
305     m_sql << "INSERT OR IGNORE INTO userdb.desc VALUES " << "('version', '1.2.0');\n"
306           << "INSERT OR IGNORE INTO userdb.desc VALUES " << "('uuid', '" << UUID () << "');\n"
307           << "INSERT OR IGNORE INTO userdb.desc VALUES " << "('hostname', '" << Hostname () << "');\n"
308           << "INSERT OR IGNORE INTO userdb.desc VALUES " << "('username', '" << Env ("USERNAME") << "');\n"
309           << "INSERT OR IGNORE INTO userdb.desc VALUES " << "('create-time', datetime());\n"
310           << "INSERT OR IGNORE INTO userdb.desc VALUES " << "('attach-time', datetime());\n";
311
312     /* create phrase tables */
313     for (guint i = 0; i < MAX_PHRASE_LEN; i++) {
314         m_sql.appendPrintf ("CREATE TABLE IF NOT EXISTS userdb.py_phrase_%d (user_freq, phrase TEXT, freq INTEGER ", i);
315         for (guint j = 0; j <= i; j++)
316             m_sql.appendPrintf (",s%d INTEGER, y%d INTEGER", j, j);
317         m_sql << ");\n";
318     }
319
320     /* create index */
321     m_sql << "CREATE UNIQUE INDEX IF NOT EXISTS " << "userdb.index_0_0 ON py_phrase_0(s0,y0,phrase);\n";
322     m_sql << "CREATE UNIQUE INDEX IF NOT EXISTS " << "userdb.index_1_0 ON py_phrase_1(s0,y0,s1,y1,phrase);\n";
323     m_sql << "CREATE INDEX IF NOT EXISTS " << "userdb.index_1_1 ON py_phrase_1(s0,s1,y1);\n";
324     for (guint i = 2; i < MAX_PHRASE_LEN; i++) {
325         m_sql << "CREATE UNIQUE INDEX IF NOT EXISTS " << "userdb.index_" << i << "_0 ON py_phrase_" << i
326               << "(s0,y0";
327         for (guint j = 1; j <= i; j++)
328             m_sql << ",s" << j << ",y" << j;
329         m_sql << ",phrase);\n";
330         m_sql << "CREATE INDEX IF NOT EXISTS " << "userdb.index_" << i << "_1 ON py_phrase_" << i << "(s0,s1,s2,y2);\n";
331     }
332     m_sql << "COMMIT;";
333
334     if (!executeSQL (m_sql))
335         goto _failed;
336
337     m_sql  = "UPDATE userdb.desc SET value=datetime() WHERE name='attach-time';";
338
339     if (!executeSQL (m_sql))
340         goto _failed;
341
342     return TRUE;
343
344 _failed:
345     m_sql = "DETACH DATABASE userdb;";
346     executeSQL (m_sql);
347     return FALSE;
348 }
349
350 void
351 Database::prefetch (void)
352 {
353     m_sql.clear ();
354     for (guint i = 0; i < DB_PREFETCH_LEN; i++)
355         m_sql << "SELECT * FROM py_phrase_" << i << ";\n";
356
357     // g_debug ("prefetching ...");
358     executeSQL (m_sql);
359     // g_debug ("done");
360 }
361
362 inline static gboolean
363 pinyin_option_check_sheng (guint option, gint id, gint fid)
364 {
365     switch ((id << 16) | fid) {
366     case (PINYIN_ID_C << 16) | PINYIN_ID_CH:
367         return (option & PINYIN_FUZZY_C_CH);
368     case (PINYIN_ID_CH << 16) | PINYIN_ID_C:
369         return (option & PINYIN_FUZZY_CH_C);
370     case (PINYIN_ID_Z << 16) | PINYIN_ID_ZH:
371         return (option & PINYIN_FUZZY_Z_ZH);
372     case (PINYIN_ID_ZH << 16) | PINYIN_ID_Z:
373         return (option & PINYIN_FUZZY_ZH_Z);
374     case (PINYIN_ID_S << 16) | PINYIN_ID_SH:
375         return (option & PINYIN_FUZZY_S_SH);
376     case (PINYIN_ID_SH << 16) | PINYIN_ID_S:
377         return (option & PINYIN_FUZZY_SH_S);
378     case (PINYIN_ID_L << 16) | PINYIN_ID_N:
379         return (option & PINYIN_FUZZY_L_N);
380     case (PINYIN_ID_N << 16) | PINYIN_ID_L:
381         return (option & PINYIN_FUZZY_N_L);
382     case (PINYIN_ID_F << 16) | PINYIN_ID_H:
383         return (option & PINYIN_FUZZY_F_H);
384     case (PINYIN_ID_H << 16) | PINYIN_ID_F:
385         return (option & PINYIN_FUZZY_H_F);
386     case (PINYIN_ID_L << 16) | PINYIN_ID_R:
387         return (option & PINYIN_FUZZY_L_R);
388     case (PINYIN_ID_R << 16) | PINYIN_ID_L:
389         return (option & PINYIN_FUZZY_R_L);
390     case (PINYIN_ID_K << 16) | PINYIN_ID_G:
391         return (option & PINYIN_FUZZY_K_G);
392     case (PINYIN_ID_G << 16) | PINYIN_ID_K:
393         return (option & PINYIN_FUZZY_G_K);
394     default: return FALSE;
395     }
396 }
397
398 inline static gboolean
399 pinyin_option_check_yun (guint option, gint id, gint fid)
400 {
401     switch ((id << 16) | fid) {
402     case (PINYIN_ID_AN << 16) | PINYIN_ID_ANG:
403         return (option & PINYIN_FUZZY_AN_ANG);
404     case (PINYIN_ID_ANG << 16) | PINYIN_ID_AN:
405         return (option & PINYIN_FUZZY_ANG_AN);
406     case (PINYIN_ID_EN << 16) | PINYIN_ID_ENG:
407         return (option & PINYIN_FUZZY_EN_ENG);
408     case (PINYIN_ID_ENG << 16) | PINYIN_ID_EN:
409         return (option & PINYIN_FUZZY_ENG_EN);
410     case (PINYIN_ID_IN << 16) | PINYIN_ID_ING:
411         return (option & PINYIN_FUZZY_IN_ING);
412     case (PINYIN_ID_ING << 16) | PINYIN_ID_IN:
413         return (option & PINYIN_FUZZY_ING_IN);
414     case (PINYIN_ID_IAN << 16) | PINYIN_ID_IANG:
415         return (option & PINYIN_FUZZY_IAN_IANG);
416     case (PINYIN_ID_IANG << 16) | PINYIN_ID_IAN:
417         return (option & PINYIN_FUZZY_IANG_IAN);
418     case (PINYIN_ID_UAN << 16) | PINYIN_ID_UANG:
419         return (option & PINYIN_FUZZY_UAN_UANG);
420     case (PINYIN_ID_UANG << 16) | PINYIN_ID_UAN:
421         return (option & PINYIN_FUZZY_UANG_UAN);
422     default: return FALSE;
423     }
424 }
425
426 SQLStmtPtr
427 Database::query (const PinyinArray &pinyin,
428                  guint              pinyin_begin,
429                  guint              pinyin_len,
430                  gint               m,
431                  guint              option)
432 {
433     g_assert (pinyin_begin < pinyin.size ());
434     g_assert (pinyin_len <= pinyin.size () - pinyin_begin);
435     g_assert (pinyin_len <= MAX_PHRASE_LEN);
436
437     /* prepare sql */
438     Conditions conditions;
439
440     for (guint i = 0; i < pinyin_len; i++) {
441         const Pinyin *p;
442         gboolean fs1, fs2;
443         p = pinyin[i + pinyin_begin];
444
445         fs1 = pinyin_option_check_sheng (option, p->pinyin_id[0].sheng, p->pinyin_id[1].sheng);
446         fs2 = pinyin_option_check_sheng (option, p->pinyin_id[0].sheng, p->pinyin_id[2].sheng);
447
448         if (G_LIKELY (i > 0))
449             conditions.appendPrintf (0, conditions.size (),
450                                        " AND ");
451
452         if (G_UNLIKELY (fs1 || fs2)) {
453             if (G_LIKELY (i < DB_INDEX_SIZE)) {
454                 if (fs1 && fs2 == 0) {
455                     conditions.double_ ();
456                     conditions.appendPrintf (0, conditions.size ()  >> 1,
457                                                "s%d=%d", i, p->pinyin_id[0].sheng);
458                     conditions.appendPrintf (conditions.size () >> 1, conditions.size (),
459                                                "s%d=%d", i, p->pinyin_id[1].sheng);
460                 }
461                 else if (fs1 == 0 && fs2) {
462                     conditions.double_ ();
463                     conditions.appendPrintf (0, conditions.size ()  >> 1,
464                                                "s%d=%d", i, p->pinyin_id[0].sheng);
465                     conditions.appendPrintf (conditions.size () >> 1, conditions.size (),
466                                                "s%d=%d", i, p->pinyin_id[2].sheng);
467                 }
468                 else {
469                     gint len = conditions.size ();
470                     conditions.triple ();
471                     conditions.appendPrintf (0, len,
472                                                "s%d=%d", i, p->pinyin_id[0].sheng);
473                     conditions.appendPrintf (len, len << 1,
474                                                "s%d=%d", i, p->pinyin_id[1].sheng);
475                     conditions.appendPrintf (len << 1, conditions.size (),
476                                                "s%d=%d", i, p->pinyin_id[2].sheng);
477                 }
478             }
479             else {
480                 if (fs1 && fs2 == 0) {
481                     conditions.appendPrintf (0, conditions.size (),
482                                                "s%d IN (%d,%d)", i, p->pinyin_id[0].sheng, p->pinyin_id[1].sheng);
483                 }
484                 else if (fs1 == 0 && fs2) {
485                     conditions.appendPrintf (0, conditions.size (),
486                                                "s%d IN (%d,%d)", i, p->pinyin_id[0].sheng, p->pinyin_id[2].sheng);
487                 }
488                 else {
489                     conditions.appendPrintf (0, conditions.size (),
490                                                "s%d IN (%d,%d,%d)", i, p->pinyin_id[0].sheng, p->pinyin_id[1].sheng, p->pinyin_id[2].sheng);
491                 }
492             }
493         }
494         else {
495             conditions.appendPrintf (0, conditions.size (),
496                                        "s%d=%d", i, p->pinyin_id[0].sheng);
497         }
498
499         if (p->pinyin_id[0].yun != PINYIN_ID_ZERO) {
500             if (pinyin_option_check_yun (option, p->pinyin_id[0].yun, p->pinyin_id[1].yun)) {
501                 if (G_LIKELY (i < DB_INDEX_SIZE)) {
502                     conditions.double_ ();
503                     conditions.appendPrintf (0, conditions.size ()  >> 1,
504                                                " AND y%d=%d", i, p->pinyin_id[0].yun);
505                     conditions.appendPrintf (conditions.size () >> 1, conditions.size (),
506                                                " and y%d=%d", i, p->pinyin_id[1].yun);
507                 }
508                 else {
509                     conditions.appendPrintf (0, conditions.size (),
510                                                " AND y%d IN (%d,%d)", i, p->pinyin_id[0].yun, p->pinyin_id[1].yun);
511                 }
512             }
513             else {
514                 conditions.appendPrintf (0, conditions.size (),
515                                            " AND y%d=%d", i, p->pinyin_id[0].yun);
516             }
517         }
518     }
519
520
521     m_buffer.clear ();
522     for (guint i = 0; i < conditions.size (); i++) {
523         if (G_UNLIKELY (i == 0))
524             m_buffer << "  (" << conditions[i] << ")\n";
525         else
526             m_buffer << "  OR (" << conditions[i] << ")\n";
527     }
528
529     m_sql.clear ();
530     gint id = pinyin_len - 1;
531     m_sql << "SELECT * FROM ("
532                 "SELECT 0 AS user_freq, * FROM main.py_phrase_" << id << " WHERE " << m_buffer << " UNION ALL "
533                 "SELECT * FROM userdb.py_phrase_" << id << " WHERE " << m_buffer << ") "
534                     "GROUP BY phrase ORDER BY user_freq DESC, freq DESC";
535     if (m > 0)
536         m_sql << " LIMIT " << m;
537 #if 0
538     g_debug ("sql =\n%s", m_sql.c_str ());
539 #endif
540
541     /* query database */
542     SQLStmtPtr stmt (new SQLStmt (m_db));
543
544     if (!stmt->prepare (m_sql)) {
545         stmt.reset ();
546     }
547
548     return stmt;
549 }
550
551 inline void
552 Database::phraseWhereSql (const Phrase & p, String & sql)
553 {
554     sql << " WHERE";
555     sql << " s0=" << p.pinyin_id[0].sheng
556         << " AND y0=" << p.pinyin_id[0].yun;
557     for (guint i = 1; i < p.len; i++) {
558         sql << " AND s" << i << '=' << p.pinyin_id[i].sheng
559             << " AND y" << i << '=' << p.pinyin_id[i].yun;
560     }
561     sql << " AND phrase=\"" << p.phrase << "\"";
562
563 }
564
565 inline void
566 Database::phraseSql (const Phrase & p, String & sql)
567 {
568     sql << "INSERT OR IGNORE INTO userdb.py_phrase_" << p.len - 1
569         << " VALUES(" << 0                  /* user_freq */
570         << ",\"" << p.phrase << '"'         /* phrase */
571         << ','   << p.freq;                 /* freq */
572
573     for (guint i = 0; i < p.len; i++) {
574         sql << ',' << p.pinyin_id[i].sheng << ',' << p.pinyin_id[i].yun;
575     }
576
577     sql << ");\n";
578
579     sql << "UPDATE userdb.py_phrase_" << p.len - 1
580         << " SET user_freq=user_freq+1";
581
582     phraseWhereSql (p, sql);
583     sql << ";\n";
584 }
585
586 void
587 Database::commit (const PhraseArray  &phrases)
588 {
589     Phrase phrase = {""};
590
591     m_sql = "BEGIN TRANSACTION;\n";
592     for (guint i = 0; i < phrases.size (); i++) {
593         phrase += phrases[i];
594         phraseSql (phrases[i], m_sql);
595     }
596     if (phrases.size () > 1)
597         phraseSql (phrase, m_sql);
598     m_sql << "COMMIT;\n";
599
600     executeSQL (m_sql);
601 }
602
603 void
604 Database::remove (const Phrase & phrase)
605 {
606     m_sql = "BEGIN TRANSACTION;\n";
607     m_sql << "DELETE FROM userdb.py_phrase_" << phrase.len - 1;
608     phraseWhereSql (phrase, m_sql);
609     m_sql << ";\n";
610     m_sql << "COMMIT;\n";
611
612     executeSQL (m_sql);
613 }
614
615 void
616 Database::init (void)
617 {
618     if (m_instance == NULL) {
619         m_instance.reset (new Database ());
620     }
621 }
622
623 };