root/dev/common/class.db_cache.php @ 184

Revision 184, 8.2 KB (checked in by exi, 16 years ago)

Fix for ticket:39 in class.db_cache.php.
Several additions to the parser and cache loader to support more sql statements.

Line 
1<?php
2
3class DBConnection
4{
5    function DBConnection()
6    {
7        if (!$this->id_ = mysql_pconnect(DB_HOST, DB_USER, DB_PASS))
8            die("Unable to connect to mysql database.");
9
10        mysql_select_db(DB_NAME);
11    }
12
13    function id()
14    {
15        return $this->id_;
16    }
17
18    function affectedRows()
19    {
20        return mysql_affected_rows($this->id_);
21    }
22}
23
24class DBQuery
25{
26    function DBQuery()
27    {
28        $this->executed_ = false;
29        $this->_cache = array();
30        $this->_cached = false;
31
32        // this is the minimum runtime a query has to run to be
33        // eligible for caching in seconds
34        $this->_minruntime = 0.1;
35
36        // maximum size of a cached result set (512kB)
37        $this->_maxcachesize = 524288;
38    }
39
40    function checkCache()
41    {
42        // only cache selects
43        // we don't use select ... into so there is no problem
44        if (strtolower(substr($this->_sql, 0, 6)) != 'select' && strtolower(substr($this->_sql, 0, 4)) != 'show')
45        {
46            // this is no select, update the table
47            $this->markAffectedTables();
48            return false;
49        }
50
51        if (file_exists(KB_CACHEDIR.'/qcache_qry_'.$this->_hash))
52        {
53            $this->_mtime = filemtime(KB_CACHEDIR.'/qcache_qry_'.$this->_hash);
54            if ($this->isCacheValid())
55            {
56                return true;
57            }
58        }
59
60        return false;
61    }
62
63    function parseSQL()
64    {
65        // gets all involved tables for a select statement
66        $text = strtolower($this->_sql).' ';
67
68        // we try to get the text from 'from' to 'where' because all involved
69        // tables are declared in that part
70        $from = strpos($text, 'from')+5;
71        if (!$to = strpos($text, 'where'))
72        {
73            $to = strlen($text);
74        }
75        $parse = trim(substr($text, $from, $to-$from));
76
77        $tables = array();
78        if (strpos($parse, ',') !== false)
79        {
80            // , is a synonym for join so we'll replace them
81            $parse = str_replace(',', ' join ', $parse);
82        }
83        if (strpos($parse, 'join'))
84        {
85            // if this query is a join we parse it with regexp to get all tables
86            preg_match_all('/join (.*?) /', $parse, $match);
87            $tables = $match[1];
88        }
89        else
90        {
91            // no join so it is hopefully a simple table select
92            $tables[] = $parse;
93        }
94
95        $this->_usedtables = $tables;
96    }
97
98    function isCacheValid()
99    {
100        // check if cachefiles are stil valid
101
102        // first, we need to get all involved tables
103        $this->parseSQL();
104
105        foreach ($this->_usedtables as $table)
106        {
107            $file = KB_CACHEDIR.'/qcache_tbl_'.$table;
108            if (file_exists($file))
109            {
110                // if one of the tables is outdated, the query is outdated
111                if ($this->_mtime < filemtime($file))
112                {
113                    return false;
114                }
115            }
116        }
117        return true;
118    }
119
120    function markAffectedTables()
121    {
122        // this function invalidates cache files for touched tables
123        $text = trim(strtolower($this->_sql));
124        $text = str_replace(array('ignore','`'), '', $text);
125        $ta = preg_split('/ /', $text, 0, PREG_SPLIT_NO_EMPTY);
126
127        // check for sql keywords and get the table from the appropriate position
128        $tables = array();
129        if ($ta[0] == 'update')
130        {
131            $tables[] = $ta[1];
132        }
133        elseif ($ta[0] == 'insert')
134        {
135            $tables[] = $ta[2];
136        }
137        elseif ($ta[0] == 'replace')
138        {
139            $tables[] = $ta[2];
140        }
141        elseif ($ta[0] == 'delete')
142        {
143            $tables[] = $ta[2];
144        }
145        else
146        {
147            var_dump($ta);
148            trigger_error('No suitable handler for query found.',E_USER_WARNING);
149            return false;
150        }
151
152        foreach ($tables as $table)
153        {
154            $file = KB_CACHEDIR.'/qcache_tbl_'.$table;
155            touch($file);
156        }
157        // refresh php's filestatcache so we dont get wrong timestamps on changed files
158        clearstatcache();
159    }
160
161    function genCache()
162    {
163        // this function fetches all rows and writes the data into a textfile
164
165        // don't attemp to cache updates!
166        if (strtolower(substr($this->_sql, 0, 6)) != 'select' && strtolower(substr($this->_sql, 0, 4)) != 'show')
167        {
168            return false;
169        }
170
171        $bsize = 0;
172        while ($row = $this->getRow())
173        {
174            $this->_cache[] = $row;
175
176            // if the bytesize of the table exceeds the limit we'll abort
177            // the cache generation and leave this query unbuffered
178            $bsize += join('', $row);
179            if ($bsize > $this->_maxcachesize)
180            {
181                $this->_cache[] = array();
182                $this->_cached = false;
183                $this->rewind();
184                return false;
185            }
186        }
187
188        // write data into textfile
189        file_put_contents(KB_CACHEDIR.'/qcache_qry_'.$this->_hash, serialize($this->_cache));
190
191        $this->_cached = true;
192        $this->_currrow = 0;
193        $this->executed_ = true;
194    }
195
196    function loadCache()
197    {
198        // loads the cachefile into the memory
199        $this->_cache = unserialize(file_get_contents(KB_CACHEDIR.'/qcache_qry_'.$this->_hash));
200
201        $this->_cached = true;
202        $this->_currrow = 0;
203        $this->executed_ = true;
204    }
205
206    function execute($sql)
207    {
208        $this->_sql = trim($sql);
209        $this->_hash = md5($this->_sql);
210
211        if ($this->checkCache())
212        {
213            $this->loadCache();
214            return true;
215        }
216
217        // we got no or no valid cache so open the connection and run the query
218        $this->dbconn_ = new DBConnection;
219
220        $t1 = strtok(microtime(), ' ') + strtok('');
221
222        $this->resid_ = mysql_query($sql, $this->dbconn_->id());
223
224        if ($this->resid_ == false)
225        {
226            if (DB_HALTONERROR === true)
227            {
228                echo "Database error: ".mysql_error($this->dbconn_->id())."<br/>";
229                echo "SQL: ".$this->_sql."<br/>";
230                exit;
231            }
232            else
233            {
234                return false;
235            }
236        }
237
238        $this->exectime_ = strtok(microtime(), ' ') + strtok('') - $t1;
239        $this->executed_ = true;
240
241        if (KB_PROFILE == 2)
242        {
243            file_put_contents('/tmp/profile.lst', $sql."\nExecution time: ".$this->exectime_."\n", FILE_APPEND);
244        }
245
246        // if the query was too slow we'll fetch all rows and run it cached
247        if ($this->exectime_ > $this->_minruntime)
248        {
249            $this->genCache();
250        }
251
252        return true;
253    }
254
255    function recordCount()
256    {
257        if ($this->_cached)
258        {
259            return count($this->_cache);
260        }
261        return mysql_num_rows($this->resid_);
262    }
263
264    function getRow()
265    {
266        if ($this->_cached)
267        {
268            if (!isset($this->_cache[$this->_currrow]))
269            {
270                return false;
271            }
272            // return the current row and increase the pointer by one
273            return $this->_cache[$this->_currrow++];
274        }
275        if (is_resource($this->resid_))
276        {
277            return mysql_fetch_assoc($this->resid_);
278        }
279        return false;
280    }
281
282    function rewind()
283    {
284        if ($this->_cached)
285        {
286            $this->_currrow = 0;
287        }
288        @mysql_data_seek($this->resid_, 0);
289    }
290
291    function getInsertID()
292    {
293        return mysql_insert_id();
294    }
295
296    function execTime()
297    {
298        return $this->exectime_;
299    }
300
301    function executed()
302    {
303        return $this->executed_;
304    }
305
306    function getErrorMsg()
307    {
308        $msg = $this->sql_."<br>";
309        $msg .= "Query failed. ".mysql_error($this->dbconn_->id());
310
311        return $msg;
312    }
313}
314?>
Note: See TracBrowser for help on using the browser.