Squirrel/Examples/SQLite

From Liberty Unleashed Wiki
Jump to: navigation, search

This is a basic tutorial on how to use the built-in SQLite functions. The tutorial also introduces a simple script which implements an SQLite database to save the stats of your server. Note that SQLite functions require the external module lu_sqlite.

Contents


Creating/restoring a database

First thing to do is to load/create a new SQLite database. This is done with the function sqlite_open. It creates a new database and saves it using the provided filename; if the specified file exists, the function attempts to load the database from it and use it.

Our example script loads or creates a database for the statistics script. It loads the player data on join and saves it when they leave.

  1.  
  2. function onScriptLoad()
  3. {
  4. // Load the module which provides SQLite functions
  5. LoadModule( "lu_sqlite" );
  6. // Create/load the database and store it to a global variable 'statDB'
  7. statDB <- sqlite_open( "serverstats.sqlite" );
  8. // Create an array to store the stats for every player while they are ingame
  9. stats <- array( GetMaxPlayers(), null );
  10. return 1;
  11. }
  12.  

For future usage we are saving the database to a global variable. We will also create an array which will contain player data while they are ingame, in order to prevent unnecessary querying of the database.

To save player stats into the array, we need a table or a class. This example creates a class which contains the basic statistics for one player. The objects of this class will be saved into the array created earlier. For more info about Squirrel classes, see the Squirrel reference manual.

  1.  
  2. class PlayerStats
  3. {
  4. Kills = 0;
  5. Deaths = 0;
  6. Joins = 0;
  7. PreviousData = false; // This variable will tell us later whether the player has any previous stats saved in the table
  8. }
  9.  

Creating a table

Next the database needs a table to store the data. Note that this only has to be done once, after that the table will stay in the database until it is removed by the user.

To execute commands, for example create tables, read/write data, we need to query the database with the wanted command. In SQL new tables are created with the command 'CREATE TABLE'. The server function for querying the database is sqlite_query, which accepts the earlier retrieved database pointer and the query as a string as its parameters. Remember that the query pointer should always be freed after processing using sqlite_free to prevent possible memory leaks.

This example creates a table called Stats, which contains player name (string, length may vary), their kills and deaths and the number of times they have joined the server (integers).

  1.  
  2. sqlite_query( statDB, "CREATE TABLE Stats ( Name VARCHAR(32), Kills INT, Deaths INT, Joins INT )" );
  3.  

Since this only has to be done once, it could be added for example as a console input command, and executed when the server is started for the first time.

Also note that 'CREATE TABLE' doesn't return any value, thus sqlite_query will return null.

Returning data from a table

When a player joins the server, their previously stored stats should be loaded from the database. In this example the stats are temporarily stored into an object related to the player, loaded from the database on join and stored back on part.

When querying the database with a query that returns data, the function sqlite_query returns an object which contains the query result: all the rows and columns. To retrieve the info from this object we will have iterate through the result table rows and fetch the wanted data from each column.

Iterating through the rows is done with sqlite_next_row function. The function takes the query result object and modifies it so that the next row of the result is available. Then you can pick the row, get its column count with sqlite_column_count and fetch the data in a specific column with sqlite_column_data. sqlite_column_count takes the query result object as a parameter. sqlite_column_count accepts the query result object and the index of the wanted column as an integer.

Let's assume we have the following table saved to our database:

  1.  
  2. -------------------------------------
  3. | ~ Stats ~ |
  4. -------------------------------------
  5. | Name | Kills | Deaths | Joins |
  6. |------------------------------------
  7. | player1 | 23 | 12 | 14 |
  8. | player2 | 123 | 377 | 93 |
  9. | player3 | 99 | 999 | 99 |
  10. | someone | 78 | 34 | 7 |
  11. | spaner | 1337 | 429 | 152 |
  12. | tammer | 38 | 3482 | 392 |
  13. -------------------------------------
  14.  

If we want to retrieve the kills for players whose nickname is 'Player*', we would query the server with "SELECT Name, Kills FROM Stats WHERE Name LIKE 'player%'". The result object from QuerySQL would then contain the following table:

  1.  
  2. |-------------------
  3. | player1 | 23 |
  4. | player2 | 123 |
  5. | player3 | 99 |
  6. --------------------
  7.  

Since we asked for two columns (player name and kills), sqlite_column_count( q ), where q is the query result from sqlite_query, would return 2. To get the results, we need to iterate through the rows using GetSQLNextRow. The following script shows how to print the player names and their kill amounts on the server console:

  1.  
  2. local q = sqlite_query( database, "SELECT Name, Kills FROM Stats WHERE Name LIKE 'player%'" );
  3.  
  4. print( "Result columns: " + sqlite_column_count( q ) );
  5. print( "\nPlayer name - Kills" );
  6.  
  7. // Iterate the rows in a loop until there are no more rows (column data = null)
  8. while ( sqlite_column_data( q, 0 ) != null )
  9. {
  10. // Print the data in the first (0) and second (1) column of the current row
  11. print( sqlite_column_data( q, 0 ) + " - " + GetSQLColumnData( q, 1 ) );
  12. // Read the next row available
  13. q = sqlite_next_row( q );
  14. }
  15.  

The output would be:

  1.  
  2. SCRIPT: Result columns: 2
  3.  
  4. SCRIPT: Player name - Kills
  5. SCRIPT: player1 - 23
  6. SCRIPT: player2 - 123
  7. SCRIPT: player3 - 99
  8.  

Note that sqlite_column_data converts the data automatically to either integer, float or string depending in which format the data had been saved.

Also note, thats it's important to iterate through all the rows even though you would know how many lines you want to get, because the result object will auto-destroy itself after it has returned all the rows. If this is not done, the memory slot reserved for the result will stay reserved and eventually the server will use more memory than it would need.

For our example script the reading happens when a player joins the server. The query "SELECT Kills, Deaths, Joins FROM Stats WHERE Name=<player name here>" will return player's kills, deaths and joins in a single row respectively.

  1.  
  2. function onPlayerJoin( player )
  3. {
  4. // Create a temporal stat object to store the player stats while they're online
  5. local id = player.ID;
  6. stats[ player.ID ] = PlayerStats();
  7. // Convert player name to lower case; that way we can ignore the case
  8. local name = player.Name.tolower();
  9. local q = sqlite_query( statDB, "SELECT Kills, Deaths, Joins FROM stats WHERE Name='" + name + "'" );
  10.  
  11. // If the player had previous stats saved (ie. there's value in the first column of the first row)
  12. if ( sqlite_column_data( q, 0 ) != null )
  13. {
  14. // Remember that player had previous stats in the database
  15. stats[ id ].PreviousData = true;
  16. // Fetch the asked values from the columns of the returned row
  17. stats[ id ].Kills = sqlite_column_data( q, 0 );
  18. stats[ id ].Deaths = sqlite_column_data( q, 1 );
  19. stats[ id ].Joins = sqlite_column_data( q, 2 );
  20. }
  21. // Free the query using sqlite_free
  22. sqlite_free( q );
  23. // Increase player joins
  24. stats[ id ].Joins++;
  25. return 1;
  26. }
  27.  

Saving data to a table

When the player leaves we need to save their stats into the database. This can be done with the 'INSERT INTO' command if player has no previous stats, or with 'UPDATE' if the database contains any previously saved data.

  1.  
  2. function onPlayerPart( player, partID )
  3. {
  4. // Load the player stats from the array + lowercase name
  5. local
  6. id = player.ID,
  7. name = player.Name.tolower();
  8. // If player had any data in the database, update the old stats
  9. if ( stats[ id ].PreviousData )
  10. {
  11. // Format the query string using 'format'
  12. local query = format( "UPDATE Stats SET Kills=%i, Deaths=%i, Joins=%i WHERE Name='%s'",
  13. stats[ id ].Kills, stats[ id ].Deaths, stats[ id ].Joins, name );
  14. // Then execute the query
  15. sqlite_query( statDB, query )
  16. }
  17. // Else let's just make a new entry
  18. else
  19. {
  20. local query = format( "INSERT INTO Stats (Name, Kills, Deaths, Joins) VALUES ('%s', %i, %i, %i)",
  21. name, stats[ id ].Kills, stats[ id ].Deaths, stats[ id ].Joins );
  22. sqlite_query( statDB, query );
  23. }
  24. // Reset the stat array slot
  25. stats[ id ] = null;
  26. }
  27.  

More about SQL language

This example is meant to introduce the usage of the built-in SQLite commands, not the query language itself. There are a lot of SQL related tutorials and documentations available everywhere. You can also check the following links for more details and for a list of commands/functions.

Complete example script

  1.  
  2. /////////////////////////////
  3. //// SQLite Stats Example ///
  4. /////////////////////////////
  5.  
  6. //--------------------------------------------------
  7.  
  8. function onScriptLoad()
  9. {
  10. // Load the module which provides SQLite functions
  11. LoadModule( "lu_sqlite" );
  12. // Create/load the database and store it to a global variable 'statDB'
  13. statDB <- sqlite_open( "serverstats.sqlite" );
  14. // Create an array to store the stats for every player while they are ingame
  15. stats <- array( GetMaxPlayers(), null );
  16. print( "=== Loaded SQLite Stats Example ===" );
  17. return 1;
  18. }
  19.  
  20. //--------------------------------------------------
  21.  
  22. /* The class to store player stats */
  23. class PlayerStats
  24. {
  25. Kills = 0;
  26. Deaths = 0;
  27. Joins = 0;
  28. PreviousData = false;
  29. }
  30.  
  31. //--------------------------------------------------
  32.  
  33. /* A console command to initialize a table for the stats */
  34. function onConsoleInput( cmd, text )
  35. {
  36. if ( cmd == "createtable" )
  37. {
  38. sqlite_query( statDB, "CREATE TABLE Stats ( Name VARCHAR(32), Kills INT, Deaths INT, Joins INT )" );
  39. }
  40. return 1;
  41. }
  42.  
  43. //--------------------------------------------------
  44.  
  45. function onPlayerJoin( player )
  46. {
  47. // Create a temporal stat object to store the player stats while they're online
  48. local id = player.ID;
  49. stats[ player.ID ] = PlayerStats();
  50. // Convert player name to lower case; that way we can ignore the case
  51. local name = player.Name.tolower();
  52. local q = sqlite_query( statDB, "SELECT Kills, Deaths, Joins FROM stats WHERE Name='" + name + "'" );
  53.  
  54. // If the player had previous stats saved (ie. there's value in the first column of the first row)
  55. if ( sqlite_column_data( q, 0 ) != null )
  56. {
  57. // Remember that player had previous stats in the database
  58. stats[ id ].PreviousData = true;
  59. // Fetch the asked values from the columns of the returned row
  60. stats[ id ].Kills = sqlite_column_data( q, 0 );
  61. stats[ id ].Deaths = sqlite_column_data( q, 1 );
  62. stats[ id ].Joins = sqlite_column_data( q, 2 );
  63. }
  64. // Finalize query result
  65. sqlite_free( q );
  66. // Increase player joins
  67. stats[ id ].Joins++;
  68. return 1;
  69. }
  70.  
  71. //--------------------------------------------------
  72.  
  73. /* Save player stats on part */
  74. function onPlayerPart( player, partID )
  75. {
  76. // Load the player stats from the array + lowercase name
  77. local
  78. id = player.ID,
  79. name = player.Name.tolower();
  80. // If player had any data in the database, update the old stats
  81. if ( stats[ id ].PreviousData )
  82. {
  83. // Format the query string using 'format'
  84. local query = format( "UPDATE Stats SET Kills=%i, Deaths=%i, Joins=%i WHERE Name='%s'",
  85. stats[ id ].Kills, stats[ id ].Deaths, stats[ id ].Joins, name );
  86. // Then execute the query
  87. sqlite_query( statDB, query )
  88. }
  89. // Else let's just make a new entry
  90. else
  91. {
  92. local query = format( "INSERT INTO Stats (Name, Kills, Deaths, Joins) VALUES ('%s', %i, %i, %i)",
  93. name, stats[ id ].Kills, stats[ id ].Deaths, stats[ id ].Joins );
  94. sqlite_query( statDB, query );
  95. }
  96. // Reset the stat array slot
  97. stats[ id ] = null;
  98. return 1;
  99. }
  100.  
  101. //--------------------------------------------------
  102.  
  103. /*
  104. These events will affect the player stats, so we need to grab the
  105. changes and temporarily save them into the array
  106. */
  107.  
  108. function onPlayerKill( killer, player, weapon )
  109. {
  110. stats[ killer.ID ].Kills++;
  111. stats[ player.ID ].Deaths++;
  112. return 1;
  113. }
  114.  
  115. function onPlayerDeath( player, reason )
  116. {
  117. stats[ player.ID ].Deaths++;
  118. return 1;
  119. }
  120.  
  121. //--------------------------------------------------
  122.  
  123. /* A basic /c stats command */
  124. function onPlayerCommand( player, command, text )
  125. {
  126. if ( command == "stats" )
  127. {
  128. if ( text )
  129. {
  130. local plr = FindPlayer( text );
  131. if ( !plr ) MessagePlayer( "Player " + text + " is not online." );
  132. else
  133. {
  134. local plrStats = stats[ plr.ID ];
  135. MessagePlayer( plr.Name + "'s stats:", player );
  136. MessagePlayer( "Kills: " + plrStats.Kills + ", Deaths: " + plrStats.Deaths, player );
  137. MessagePlayer( "Joins: " + plrStats.Joins, player );
  138. }
  139. }
  140. else MessagePlayer( "Usage: /stats <player>", player );
  141. }
  142. return 1;
  143. }
  144.  

Related functions

These functions are provided by the official module lu_sqlite.

Personal tools
Namespaces

Variants
Actions
Navigation
scripting
Toolbox