1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
|
/**
* @file lldiskcache.cpp
* @brief The SQLite based disk cache implementation.
* @Note Eventually, this component might split into an interface
* file and multiple implemtations but for now, this is the
* only one so I think it's okay to combine everything.
*
* $LicenseInfo:firstyear=2009&license=viewerlgpl$
* Second Life Viewer Source Code
* Copyright (C) 2020, Linden Research, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
* $/LicenseInfo$
*/
#if (defined(LL_WINDOWS) || defined(LL_LINUX) || defined(LL_DARWIN))
#include "linden_common.h"
#endif
#include "lldiskcache.h"
#include <string>
#include <sstream>
#include <random>
#include <algorithm>
#include <fstream>
///////////////////////////////////////////////////////////////////////////////
//
llDiskCache::llDiskCache() :
mDataStorePath("")
{
}
llDiskCache::~llDiskCache()
{
}
///////////////////////////////////////////////////////////////////////////////
// Opens the database - typically done when the application starts
// and is complementary to close() which is called when the application
// is finisahed and exits.
// Pass in the folder and filename of the SQLite database you want to
// use or create (file doesn't have to exist but the folder must)
// Returns true or false and writes a message to console on error
bool llDiskCache::open(const std::string db_folder, const std::string db_filename)
{
mDataStorePath = db_folder;
std::string db_pathname = makeFullPath(db_filename);
// simple flags for the moment - these will likely be extended
// later on to support the SQLite mutex model for reading/writing
// simultaneously - perhaps when we look at supporting textures too
int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
if (sqlite3_open_v2(
db_pathname.c_str(),
&mDb,
flags,
nullptr // Name of VFS module to use
) != SQLITE_OK)
{
printError(__FUNCDNAME__ , "open_v2", true);
close();
return false;
}
// I elected to store the code that generates the statement
// in sepsrate functions throughout - this seemed like a cleaner
// approach than having hundreds of stmt << "REPLACE AS" lines
// interspersed in the logic code. They are all prefixed with
// 'sqlCompose' and followed by a short description.
const std::string stmt = sqlComposeCreateTable();
if (! sqliteExec(stmt, __FUNCDNAME__ ))
{
// Not sure if we need close here - if open fails, then we
// perhaps don't need to close it - TODO: look in SQLite docs
close();
return false;
}
return true;
}
///////////////////////////////////////////////////////////////////////////////
// Determines if an entry exists.
// Pass in the id and a variable that will indicate existance
// Returns true/false if function succeeded and the boolean
// you pass in will be set to true/false if entry exists or not
bool llDiskCache::exists(const std::string id, bool& exists)
{
if (!mDb)
{
printError(__FUNCDNAME__ , "mDb is invalid", false);
return false;
}
if (id.empty())
{
printError(__FUNCDNAME__ , "id is empty", false);
return false;
}
// As well as the separate function to compose the SQL statement,
// worth pointing out that the code to 'prepare' and 'step' the
// SQLite "prepared statement" has been factored out into its own
// function and used in several other functions.
const std::string stmt = sqlComposeExists(id);
sqlite3_stmt* prepared_statement = sqlitePrepareStep(__FUNCDNAME__ , stmt, SQLITE_ROW);
if (! prepared_statement)
{
return false;
}
int result_column_index = 0;
int result_count = sqlite3_column_int(prepared_statement, result_column_index);
if (sqlite3_finalize(prepared_statement) != SQLITE_OK)
{
printError(__FUNCDNAME__ , "sqlite3_finalize()", true);
return false;
}
// given the uniqueness of the ID, this can only ever be
// either 1 or 0 so this might be a bit confusing
exists = result_count > 0 ? true : false;
return true;
}
///////////////////////////////////////////////////////////////////////////////
// Given an id (likely a UUID + decoration but can be any string), a
// pointer to a blob of binary data along with its length, write the
// entry to the cache
// Returns true/false for success/failure
bool llDiskCache::put(const std::string id,
char* binary_data,
int binary_data_size)
{
if (!mDb)
{
printError(__FUNCDNAME__ , "mDb is invalid", false);
return false;
}
if (id.empty())
{
printError(__FUNCDNAME__ , "id is empty", false);
return false;
}
// < 0 is obvious but we assert the data must be at least 1 byte long
if (binary_data_size <= 0)
{
printError(__FUNCDNAME__ , "size of binary file to write is invalid", false);
return false;
}
// we generate a unique filename for the actual data itself
// which is stored on disk directly and not in the database.
// TODO: consider making the filename more like the ID passed in
// although the problem with that is we would have to parse the id
// to remove invalid filename chars, consider length etc. As it
// stands, we can write a simple SQL statement to return the filename
// given the ID.
const std::string filename = makeUniqueFilename();
const std::string filepath = makeFullPath(filename);
std::ofstream file(filepath, std::ios::out | std::ios::binary);
if (! file.is_open())
{
std::ostringstream error;
error << "Unable to open " << filepath << " for writing";
printError(__FUNCDNAME__ , error.str(), false);
return false;
}
file.write((char*)binary_data, binary_data_size);
file.close();
// I think is a catchall "wasn't able to write the file to disk"
// conditional but should revisit when we hook this up to viewer
// code to make sure - we never want to write bad/no data to the
// disk and not indicate something has gone wrong
if (file.bad())
{
std::ostringstream error;
error << "Unable to write " << binary_data_size;
error << " bytes to " << filepath;
printError(__FUNCDNAME__ , error.str(), false);
return false;
}
// this is where the filename/size is written to the database along
// with the current date/time for the created/last access times
const std::string stmt = sqlComposePut(id, filename, binary_data_size);
if (! sqlitePrepareStep(__FUNCDNAME__ , stmt, SQLITE_DONE))
{
return false;
}
return true;
}
///////////////////////////////////////////////////////////////////////////////
// Given an id (likely a UUID + decoration but can be any string), the
// address of a pointer that will be used to allocate memory and a
// varialble that will contain the length of the data, get an item from
// the cache. Note that the memory buffer returned belongs to the calling
// function and it is its responsiblity to clean it up when it's no
// longer needed.
// Returns true/false for success/failure
const bool llDiskCache::get(const std::string id,
char** mem_buffer,
int& mem_buffer_size)
{
// Check if the entry exists first to avoid dealing with a bunch
// of conditions that look like failure but aren't in the main code.
// Note exists() is a public method and also tests for mDB and id
// being valid so we can safely put this about the same tests
// in this function
bool get_exists = false;
if (! exists(id, get_exists))
{
return false;
}
if (! get_exists)
{
return false;
}
if (!mDb)
{
printError(__FUNCDNAME__ , "mDb is invalid", false);
return false;
}
if (id.empty())
{
printError(__FUNCDNAME__ , "id is empty", false);
return false;
}
const std::string stmt_select = sqlComposeGetSelect(id);
sqlite3_stmt* prepared_statement = sqlitePrepareStep(__FUNCDNAME__ , stmt_select, SQLITE_ROW);
if (! prepared_statement)
{
return false;
}
int result_column_index = 0;
const unsigned char* text = sqlite3_column_text(prepared_statement, result_column_index);
if (text == nullptr)
{
printError(__FUNCDNAME__ , "filename is nullptr", true);
return false;
}
const std::string filename = std::string(reinterpret_cast<const char*>(text));
const std::string filepath = makeFullPath(filename);
result_column_index = 1;
int filesize_db = sqlite3_column_int(prepared_statement, result_column_index);
if (filesize_db <= 0)
{
printError(__FUNCDNAME__ , "filesize is invalid", true);
return false;
}
if (sqlite3_finalize(prepared_statement) != SQLITE_OK)
{
printError(__FUNCDNAME__ , "sqlite3_finalize()", true);
return false;
}
// Now we have the fiename, we can read the file from disk
std::ifstream file(filepath, std::ios::in | std::ios::binary | std::ios::ate);
if (! file.is_open())
{
std::ostringstream error;
error << "Unable to open " << filepath << " for reading";
printError(__FUNCDNAME__ , error.str(), false);
return false;
}
// we get the expected filesize from the database but we can also
// get it (easily) when we read the file from the disk. We compare
// the two and return false if they don't match
std::streampos filesize_file = file.tellg();
if (filesize_db != filesize_file)
{
std::ostringstream error;
error << "File size from DB (" << filesize_db << ")";
error << " and ";
error << "file size from file (" << filesize_file << ")";
error << " in file " << filepath << " are different";
printError(__FUNCDNAME__ , error.str(), false);
return false;
}
// doest matter if we choose DB or file version - they must be
// identical if we get this far - just used for clarity
int filesize = filesize_db;
// allocate a block of memory that we pass back for the calling
// function to use then delete when it's no longer needed
*mem_buffer = new char[filesize];
mem_buffer_size = filesize;
file.seekg(0, std::ios::beg);
file.read(*mem_buffer, filesize);
file.close();
if (file.bad())
{
std::ostringstream error;
error << "Unable to read " << filesize;
error << " bytes from " << filepath;
printError(__FUNCDNAME__ , error.str(), false);
return false;
}
// here we update the count of times the file is accessed so
// we can keep track of how many times it's been requested.
// This will be useful for metrics and perhaps determining
// if a file should not be purged even though its age
// might suggest that it should.
// In addition, this is where the time of last access is updated
// in the database and that us used to determine what is purged
// in an LRU fashion when the purge function is called.
const std::string stmt_update = sqlComposeGetUpdate(id);
if (! sqliteExec(stmt_update, __FUNCDNAME__ ))
{
return false;
}
return true;
}
///////////////////////////////////////////////////////////////////////////////
// Purges the database of older entries using an LRU approach.
// Pass in the number of entries to retain.
// This is called now after open to "clean up" the cache when the
// application starts.
// TODO: IMPORTANT: compose a list of files that will be deleted
// and delete them from disk too - not just from the DB
bool llDiskCache::purge(int num_entries)
{
if (num_entries < 0)
{
printError(__FUNCDNAME__ , "number of entries to purge is invalid", false);
return false;
}
// find the rows affected and get the filenames for them
//swww
// delete oldest entries leaving the correct number in place
const std::string stmt = sqlComposePurge(num_entries);
if (! sqliteExec(stmt, __FUNCDNAME__ ))
{
return false;
}
return true;
}
///////////////////////////////////////////////////////////////////////////////
// Call at application shutdown
void llDiskCache::close()
{
sqlite3_close(mDb);
}
///////////////////////////////////////////////////////////////////////////////
// Determine the version of SQLite in use
// TODO: make this a static so we can get to it from the Viewer About
// box without instantiating the whole thing.
const std::string llDiskCache::dbVersion()
{
std::ostringstream version;
version << sqlite3_libversion();
return version.str();
}
///////////////////////////////////////////////////////////////////////////////
// Given an id, return the matching filename
const std::string llDiskCache::getFilenameById(const std::string id)
{
// TODO:
return std::string();
}
///////////////////////////////////////////////////////////////////////////////
// Given an id, return the number of times that entry has been
// accessed from the cache
const int llDiskCache::getAccessCountById(const std::string id)
{
// TODO:
return -1;
}
///////////////////////////////////////////////////////////////////////////////
// Return the number of entries currently in the cache as well as
// the maximum possible entries.
void llDiskCache::getNumEntries(int& num_entries, int& max_entries)
{
num_entries = -1;
max_entries = -1;
}
///////////////////////////////////////////////////////////////////////////////
// Wraps the sqlite3_exec(..) used in many places
bool llDiskCache::sqliteExec(const std::string stmt,
const std::string funcname)
{
if (sqlite3_exec(
mDb,
stmt.c_str(),
nullptr, // Callback function (unused)
nullptr, // 1st argument to callback (unused)
nullptr // Error msg written here (unused)
) != SQLITE_OK)
{
printError(funcname, "sqlite3_exec", true);
return false;
}
return true;
}
///////////////////////////////////////////////////////////////////////////////
// Wraps the sqlite3_prepare_v2 and sqlite3_step calls used in many places
sqlite3_stmt* llDiskCache::sqlitePrepareStep(const std::string funcname,
const std::string stmt,
int sqlite_success_condition)
{
sqlite3_stmt* prepared_statement;
if (sqlite3_prepare_v2(
mDb,
stmt.c_str(),
-1, // Maximum length of zSql in bytes.
&prepared_statement,
0 // OUT: Pointer to unused portion of zSql
) != SQLITE_OK)
{
printError(funcname, "sqlite3_prepare_v2", true);
return nullptr;
}
if (sqlite3_step(prepared_statement) != sqlite_success_condition)
{
printError(funcname, "sqlite3_step", true);
sqlite3_finalize(prepared_statement);
return nullptr;
}
return prepared_statement;
}
///////////////////////////////////////////////////////////////////////////////
// When an "error" occurss - e.g. database cannot be found, file cannot be
// written, invalid argument passed into a function etc. a message is
// written to stderr that should end up in the viewer log
// TODO: Set the verbosity using the usual Viewer mechanism
void llDiskCache::printError(const std::string funcname,
const std::string desc,
bool is_sqlite_err)
{
std::ostringstream err_msg;
err_msg << "llDiskCache error in ";
err_msg << __FUNCDNAME__ << "(...) ";
err_msg << desc;
if (is_sqlite_err)
{
err_msg << " - ";
err_msg << std::string(sqlite3_errmsg(mDb));
}
// TODO: set via viewer verbosity level
const int verbosity = 1;
if (verbosity > 0)
{
std::cerr << err_msg.str() << std::endl;
}
}
///////////////////////////////////////////////////////////////////////////////
// wrapper for SQLite code to begin an SQL transaction - not used yet but
// it will be eventually
bool llDiskCache::beginTransaction()
{
const std::string stmt("BEGIN TRANSACTION");
if (! sqliteExec(stmt, __FUNCDNAME__ ))
{
return false;
}
return true;
}
///////////////////////////////////////////////////////////////////////////////
// wrapper for SQLite code to end an SQL transaction - not used yet but
// it will be eventually
bool llDiskCache::endTransaction()
{
const std::string stmt("COMMIT");
if (! sqliteExec(stmt, __FUNCDNAME__ ))
{
return false;
}
return true;
}
///////////////////////////////////////////////////////////////////////////////
// Build a unique filename that will be used to store the actual file
// on disk (as opposed to the meta data in the database)
// TODO: I think this needs more work once we move it to the viewer
// and espcially to make it cross platform - see 'std::tmpnam'
// depreciation comments in compiler output for example
std::string llDiskCache::makeUniqueFilename()
{
// TODO: replace with boost - this is marked as deprecated?
std::string base = std::tmpnam(nullptr);
// C++11 random number generation!!!
static std::random_device dev;
static std::mt19937 rng(dev());
std::uniform_int_distribution<int> dist(100000, 999999);
// currently the tmp filename from std::tmpnam() on macOS
// is of the form `/tmp/foo/bar.12345 and the following code
// strips all the preceding dirs - we likely want a more
// robust (and cross platform solution) when we move to the
// viewer code
std::size_t found = base.rfind(systemSeparator());
if (found != std::string::npos && found < base.size() - 2)
{
base = base.substr(found + 1);
}
else
{
base = "";
}
// we mix in a random number for some more entropy..
// (i know, i know...)
std::ostringstream unique_filename;
unique_filename << base;
unique_filename << ".";
unique_filename << dist(rng);
return unique_filename.str();
}
///////////////////////////////////////////////////////////////////////////////
// Return system file/path separator - likely replaced by the version
// in the viewer
const std::string llDiskCache::systemSeparator()
{
// TODO: replace in viewer with relevant call
#ifdef _WIN32
return "\\";
#else
return "/";
#endif
}
///////////////////////////////////////////////////////////////////////////////
// Given a filename, compose a full path based on the path name passed
// in when the database was opened and the separator in play.
const std::string llDiskCache::makeFullPath(const std::string filename)
{
std::string pathname = mDataStorePath + systemSeparator() + filename;
return pathname;
}
///////////////////////////////////////////////////////////////////////////////
//
const std::string llDiskCache::sqlComposeCreateTable()
{
std::ostringstream stmt;
stmt << "CREATE TABLE IF NOT EXISTS ";
stmt << mTableName;
stmt << "(";
stmt << mIdFieldName;
stmt << " TEXT PRIMARY KEY, ";
stmt << mFilenameFieldName;
stmt << " TEXT, ";
stmt << mFilesizeFieldName;
stmt << " INTEGER DEFAULT 0, ";
stmt << mInsertionDateTimeFieldName;
stmt << " TEXT, ";
stmt << mLastAccessDateTimeFieldName;
stmt << " TEXT, ";
stmt << mAccessCountFieldName;
stmt << " INTEGER DEFAULT 0";
stmt << ")";
#ifdef SHOW_STATEMENTS
std::cout << stmt.str() << std::endl;
#endif
return stmt.str();
}
///////////////////////////////////////////////////////////////////////////////
//
const std::string llDiskCache::sqlComposeExists(const std::string id)
{
std::ostringstream stmt;
stmt << "SELECT COUNT(*) FROM ";
stmt << mTableName;
stmt << " WHERE ";
stmt << mIdFieldName;
stmt << "='";
stmt << id;
stmt << "'";
#ifdef SHOW_STATEMENTS
std::cout << stmt.str() << std::endl;
#endif
return stmt.str();
}
///////////////////////////////////////////////////////////////////////////////
// SQL statement to write an entry to the DB
// Saves id, filename (generated), file size and create/last access date
const std::string llDiskCache::sqlComposePut(const std::string id,
const std::string filename,
int binary_data_size)
{
std::ostringstream stmt;
stmt << "REPLACE INTO ";
stmt << mTableName;
stmt << "(";
stmt << mIdFieldName;
stmt << ",";
stmt << mFilenameFieldName;
stmt << ",";
stmt << mFilesizeFieldName;
stmt << ",";
stmt << mInsertionDateTimeFieldName;
stmt << ",";
stmt << mLastAccessDateTimeFieldName;
stmt << ") ";
stmt << "VALUES(";
stmt << "'";
stmt << id;
stmt << "', ";
stmt << "'";
stmt << filename;
stmt << "', ";
stmt << binary_data_size;
stmt << ", ";
stmt << "strftime('%Y-%m-%d %H:%M:%f', 'now')";
stmt << ", ";
stmt << "strftime('%Y-%m-%d %H:%M:%f', 'now')";
stmt << ")";
#ifdef SHOW_STATEMENTS
std::cout << stmt.str() << std::endl;
#endif
return stmt.str();
}
///////////////////////////////////////////////////////////////////////////////
// SQL statement used in get() to look up the filename and file size
const std::string llDiskCache::sqlComposeGetSelect(const std::string id)
{
std::ostringstream stmt;
stmt << "SELECT ";
stmt << mFilenameFieldName;
stmt << ", ";
stmt << mFilesizeFieldName;
stmt << " FROM ";
stmt << mTableName;
stmt << " WHERE ";
stmt << mIdFieldName;
stmt << "='";
stmt << id;
stmt << "'";
#ifdef SHOW_STATEMENTS
std::cout << stmt.str() << std::endl;
#endif
return stmt.str();
}
///////////////////////////////////////////////////////////////////////////////
// SQL statement to update the date/time of last access as well as the
// count of number of times the file has been accessed.
// Note: the more accurate representation of date/time is used to
// ensure ms accuracy vs the standard INTEGER days since epoch approach
const std::string llDiskCache::sqlComposeGetUpdate(const std::string id)
{
std::ostringstream stmt;
stmt << "UPDATE ";
stmt << mTableName;
stmt << " SET ";
stmt << mAccessCountFieldName;
stmt << "=";
stmt << mAccessCountFieldName;
stmt << "+1";
stmt << ", ";
stmt << mLastAccessDateTimeFieldName;
stmt << "=";
stmt << "strftime('%Y-%m-%d %H:%M:%f', 'now')";
stmt << " WHERE ";
stmt << mIdFieldName;
stmt << "='";
stmt << id;
stmt << "'";
#ifdef SHOW_STATEMENTS
std::cout << stmt.str() << std::endl;
#endif
return stmt.str();
}
///////////////////////////////////////////////////////////////////////////////
// SQL statement to remove items from the database that are older
// than the newest num_elements entries
const std::string llDiskCache::sqlComposePurge(int num_entries)
{
std::ostringstream stmt;
stmt << "DELETE FROM ";
stmt << mTableName;
stmt << " WHERE ";
stmt << mLastAccessDateTimeFieldName;
stmt << " NOT IN ";
stmt << "(";
stmt << "SELECT ";
stmt << mLastAccessDateTimeFieldName;
stmt << " FROM ";
stmt << mTableName;
stmt << " ORDER BY ";
stmt << mLastAccessDateTimeFieldName;
stmt << " DESC";
stmt << " LIMIT ";
stmt << num_entries;
stmt << ")";
//#ifdef SHOW_STATEMENTS
std::cout << stmt.str() << std::endl;
//#endif
return stmt.str();
}
|