SBM ModScript, Part 12 - Class Inheritance

After looking into ChaiScript classes, I discovered that ChaiScript does not currently support class inheritance. This is common for scripting languages. However, as ChaiScript is such a powerful language, I was wondering how hard it would be to implement it. I came up with the following two options.

Option 1

Option 1 was updated on June 11, 2018 to introduce the "this.This" object, which allows for methods in the base class to invoke overrides on the child class.

def _getBaseVarName( obj ) {
  return "_${obj.get_type_name()}_base";

def _getBase( obj ) {
  return eval("obj.${_getBaseVarName(obj)}");

def _setThis( obj, ancestor ) {
  ancestor.This := obj;
  if ( eval( "!ancestor.${_getBaseVarName(ancestor)}.is_var_undef()" ) ) {
    _setThis( obj, ancestor._getBase() );

def _addMethod( obj, funcname, containedFunc ) {
  var pts = containedFunc.get_param_types();
  // since we checked arity above, we know there will be at least 2 entries, so this is safe
  if ( pts[1].is_type_undef() || pts[1].name() == "Dynamic_Object" ) { // if parameter is for a Dynamic_Object class, this is true
    // ensure there are no guards on the parameters (pts[0] is return, pts[1] is "this"
    if ( pts.size() > 2 ) {
      for( var ii = 2; ii < pts.size(); ++ii ) {
        if ( pts[ii].name() != "Object" ) {
          return false;
    var paramStr = ""
    for ( var ii = 1; ii < containedFunc.get_arity(); ++ii ) {
      if ( ii != 1 ) {
        paramStr += ',';
      paramStr += "v${ii}";
    eval( "def ${obj.get_type_name()}::${funcname}(${paramStr}){ this._getBase().${funcname}(${paramStr}); }" );
    return true;
  return false;

def _verifyAndAddMethod( obj, base, funcname, func ) {
  var sParamVals = "";
  for ( var x = 1; x < func.get_arity(); ++x ) {
    sParamVals += ",0";
  if ( eval("call_exists( func, base ${sParamVals} );") && // method exists on base object
       !eval("call_exists( func, obj ${sParamVals} );") && // method does not exist on this object (override)
       !func.has_guard() &&                                // method may not have guards
       !func.get_contained_functions()[0].has_guard() &&   // method may not have guards
       funcname != "This" &&                               // ignore "This" attribute
       !_addMethod( obj, funcname, func ) ) {
    for ( containedFunc : func.get_contained_functions() ){
      if ( !containedFunc.get_contained_functions().empty() && _addMethod( obj, funcname, containedFunc ) ) {

def _setup( obj ) {
  _setThis( obj, obj );

def _inherit( obj, base ) {
  obj._getBase() = base;
  _setup( obj );
  global _inheritance;
  if ( _inheritance.is_var_undef() ) {
    _inheritance = Map();
  // only set up this class inheritance once
  if ( _inheritance.count( obj.get_type_name() ) == 0 ) {
    _inheritance[obj.get_type_name()] = base.get_type_name();
    // must invoke get_functions every time, cannot filter and save list, as class definitions might occur after we 
    // hit this for the first time
    for ( func : get_functions() ) {
      if ( func.second.get_arity() == -1 ) {
        for ( inner : func.second.get_contained_functions() ) {
          if ( inner.get_arity() > 0 && !inner.get_contained_functions().empty() ) {
            _verifyAndAddMethod( obj, base, func.first, inner );
      else if ( func.second.get_arity() > 0 && !func.second.get_contained_functions().empty() ) {
        _verifyAndAddMethod( obj, base, func.first, func.second );
class Base {
  attr id;
  def Base() { _setup( this ); }
  def Base( ID ) { _setup( this ); = ID; }
  def go( x ){ print("go(x) | dynamicType: ${this.This.get_type_name()} | x: ${x} | id: ${}"); }
  def go2( x ){ print( "go2(x) | hardcodedType: Base | x: ${x} | id: ${}" ); }
  def go3( x, y ){ print( "go3(x,y) | hardcodedType: Base | x: ${x} | x: ${y} | id: ${}" ); }

class Child {
  def Child() { 
    _inherit( this, Base() );
  def Child( ID ) {
    _inherit( this, Base( ID ) );
  // override that (optionally) invokes base
  def go2( x ){ print( "Child: ${x}: ${}" ); this._getBase().go2( x ); }

class GrandChild {
  def GrandChild() { 
    _inherit( this, Child() );
  def GrandChild( ID ) {
    _inherit( this, Child( ID ) );
  // override that (optionally) invokes _base
  def go3( x, y ){ print( "go3(x,y) | dynamicType: ${this.get_type_name()} | x: ${x} | x: ${y}" );  this._getBase().go3( x,y ); }
What Does It Do?
  • In any given base class (class which does not inherit from another class, but will be inherited from), invoke "_setup(this);".
    • This will set up a "this.This" attribute on the object.
  • In the constructors of child classes, invoke "_inherit( this, )".
    • The class of the base object is used to determine the parent class of this object. It is important to use the same base class for every constructor for the child class. 
    • You can invoke any of the parent's constructors, allowing you to initialize the parent object as desired.
  • Behind the scenes of the _inherit() function
    • It will look at all the attributes and methods of the parent class and, if it doesn't find an override in the child class, it will define a method in the child class that invokes the method from the parent class. As such, any given child class object will have access to all the attributes and methods of the parent object.
    • The base object passed to _inherit is stored as a member on the child object. It is accessible via the "this._getBase()" method. This allows you to invoke the parent class method directly if desired.
    • Each object of type GrandChild will look like this (simplified):
      • GrandChild class
        • this = theGrandChildObj
        • this.This = theGrandChildObj
        • this._getBase() = theChildObj (Child class)
          • this = theChildObj
          • this.This = theGrandChildObj
          • this._getBase() = theBaseObj (Base class)
            • this = theBaseObj
            • this.This = theGrandChildObj
            • this._getBase() = undefinedObj
    • As you can see, in the GrandChild constructor, we create an object of type Child (which gets stored in the "this._getBase()" location). The Child constructor creates an object of type Base which gets stored in the Child object's "this._getBase()" location. Therefore, in any given method, you can call the parent object's version of the method by invoking this._getBase().method().
    • Each object will point to itself via the "this" attribute. Be careful when you use "this", as it is safer to use "this.This" to ensure polymorphic overrides are invoked.
    • Each object will point to the actual object via the "this.This" attribute. As such, any given method can polymorphically invoke the methods of the child object.
      • Do NOT use "this.This" when invoking "this._getBase()".
  • Watch out for guards on methods or parameters. It is just too complex to support class method overrides with guards, so they are not supported in this implementation. Also, each class is still separate as far as the ChaiScript class engine is concerned, so a function like "foo( Base bar )" will not recognize an object of type "Child". As long as you leave the guards out, all objects should act polymorphically correct.
  • Make sure to invoke _inherit() in every constructor on the child class, and pass in an object from the parent class.
  • Make sure to invoke _setup() in every class intended for usage as a base class. This will set up the "this.This" pointer, which allows the base class methods to invoke the overridden methods in the child class.
  • Attributes and Methods added to the parent class after a child class object has been created will never be visible to objects of the child class.
  • No classes in the class hierarchy may have methods with guards on the parameters or on the functions.
  • Objects of the child class will not convert to the base class, do not try to pass an object of type Child to a function like foo( Base bar ).
  • When invoking a method from the base class, code inside the base class method will treat the "this" object as if it is of the base class. Calls in the base method to other methods that have overrides in the child will not invoke the child override if you use the "this" object. To get the child methods to invoke from base methods, use the "this.This" object.
  • Classes built in to ModScript cannot be inherited from.


Option 2

There is another option, which has different benefits and drawbacks. The biggest drawback is that you cannot use the ChaiScript "class" syntax.

global _classInheritence_ = ["VirtualClass" : ""];
global _vtable_ = Map();

// All "classes" will be instances of VirtualClass, but with a different
// value for _classname_. It knows how to look up the stuff for the
// fake "class".
class VirtualClass{
  attr _classname_;
  def VirtualClass( sClass ) { this._classname_ = sClass; }
  // a method for invoking the function in the parent class
  def _callBase( classname, funcName, params ) {
    // verify "classname" is base of "this"
    var classnameSearch = this._classname_;
    while ( !classnameSearch.empty() ) {
      if ( classnameSearch == classname ) {
      classnameSearch = _classInheritence_[classnameSearch];
    if ( classnameSearch.empty() ) {
      Ext.LogErrorMsg( __FILE__ + ":" + __LINE__ + ": could not find base class ${classname} for ${this._classname_}");
    var f = _findVMethod( "${funcName}:${params.size()+1}", classname );
    var p2 = [this];
    params.for_each( back_inserter( p2 ) );
    return call( f, p2 );

def _findVMethod( sFuncWithArrity, classname ) {
  var virtFuncArrity = sFuncWithArrity );
  var classnameSearch = classname;
  while ( !classnameSearch.empty() ) {
    if ( virtFuncArrity.count( classnameSearch ) == 1 ) {
      return virtFuncArrity[ classnameSearch ];
    classnameSearch = _classInheritence_[classnameSearch];
  Ext.LogErrorMsg( __FILE__ + ":" + __LINE__ + ": could not find function ${sFuncWithArrity} for class ${classname}" );

def VClass( sClass, sParentClass ) {
  if ( _classInheritence_.count(sClass) == 0 ) {
    //eval( "global ${sClass} = fun(){ return VirtualClass( \"${sClass}\" ); };" );
    eval( "def ${sClass}(){ return VirtualClass( \"${sClass}\" ); }" );
    _classInheritence_[sClass] = sParentClass;

def VClass( sClass ) {
  VClass( sClass, "VirtualClass" );

def VMethod( sClass, sFunc, f ) {
  if ( f.get_arity() < 1 ) {
  else if ( f.has_guard() ) {
    Ext.LogErrorMsg( __FILE__ + ":" + __LINE__ + ": registerVirtual failed for function ${sFunc}, virtual function cannot have guard" );
  else {
    // check the params of f, reject if any params have a guard
    for ( p : f.get_param_types() ){
      if ( != "Object" ) {
        Ext.LogErrorMsg( __FILE__ + ":" + __LINE__ + ": registerVirtual failed for function ${sFunc}, virtual function cannot have guard on parameter" );
  var sFuncWithArrity = "${sFunc}:${f.get_arity()}";
  if ( _vtable_.count( sFuncWithArrity ) == 0 ) {
    _vtable_[sFuncWithArrity] = Map();
    var sParams = "";
    for ( var v = 1; v < f.get_arity(); ++v ) {
      if ( v != 1 ) {
        sParams += ',';
      sParams += "v${v}";
    // add a method to the VirtualClass that will look up the method for this "class"
"def VirtualClass::${sFunc}(${sParams}){ 
  /* _findVMethod throws if not found */
  var f = _findVMethod( \"${sFuncWithArrity}\", this._classname_ );
  return f( this ${sParams.empty() ? "" : ","} ${sParams} );
}" );
  _vtable_[sFuncWithArrity][sClass] = f;

def VClassAttr( sClass, sMember ) {
  VMethod( sClass, sMember, eval( "fun(this){ return this._${sClass}_${sMember}; }" ) );
VClass( "Base" );
VMethod( "Base", "go", fun(this, x){ print( "${this._classname_}: ${x}" ); } );
VMethod( "Base", "go2", fun(this, x){ print( "Base: ${x}" ); } );
VClassAttr( "Base", "id" );

VClass( "Child", "Base" );
VMethod( "Child", "go2", fun(this, x){ 

// optional call to base
this._callBase( "Base", "go2", [x] );
} ); VClass( "GrandChild", "Child" ); VMethod( "GrandChild", "go2", fun(this, x){ this._callBase( "Base", "go2", [x] ); } );


  • All objects are instances of the VirtualBase class
    • All methods and attributes for each class are added to the _vtable_ Map.
    • Each object has an attribute that tells it which class it is.
    • Function calls will look up the method for the current class, and if no override is found, will search up the class tree to find the method or attribute.
    • As opposed to Option 1, when invoking a method from the base class, code inside the base class method will treat the object as if it is of the child class. Calls in the base method to other methods that have overrides in the child will invoke the child override. As such, there is no need for a "this.This" attribute.
  • No virtual methods may have methods with guards on the parameters or on the functions.
  • All objects are of type VirtualBase, do not try to pass an object of type Child to a function like foo( Base bar ), but you can pass any objects from this hierarchy to a function like  foo( VirtualBase bar ).
  • Classes built in to ModScript cannot be inherited from.
  • Option 2 is faster than Option 1.
  • this._callBase() requires that you tell it the name of the base class, and it checks to ensure that the base class you select is an ancestor of the current class.


SBM ModScript Blog Series

Continue reading
1765 Hits

SBM ModScript Blog Series

SBM ModScript was introduced in SBM 11.3. This blog series is intended to supplement the SBM ModScript Guide with background information and detailed use cases.  You can find the latest version of the documentation here

  1. An Introduction to ModScript
    • Background on SBM ModScript
  2. Transition Related Items
    • An example script containing several key features of SBM ModScript, including:
      • Function definitions
      • Regular expressions
      • List iteration
      • Defining constants
      • Transitioning SBM items
      • Reading lists of SBM items
      • Using the from_json() function
      • Multi-line comments
  3. Adding Methods to a Class
    • How to use the script engine's ability to extend classes to add new methods.
  4. JSON
    • How to use the script engine's to_json() and from_json() utility functions.
  5. Algorithms and Lambdas
    • How to use the script engine's built-in algorithms, along with the optional use of lambdas (anonymous functions).
  6. Invoking DLLs
    • How to interact with custom DLLs.
  7. REST Call Into ModScript
    • How to invoke a ModScript via a URL.
  8. REST Callouts
    • How to invoke a REST endpoint from ModScript.
  9. SQL Queries
    • An introduction to the SQL interaction available in ModScript.
  10. Regular Expressions
    • How to use regular expressions in ModScript.
  11. Transitions
    • A review of how to invoke transitions on Primary/Auxiliary items in ModScript
  12. Class Inheritence
    • An experimental approach to class inheritance.
  13. Base64Encode
    • A utility function for encoding a string using base64 
  14. Checking the Type of a Variable
    • Utility functions available for interrogating the type of a var.
  15. Singletons
    • Using the singleton design pattern.
  16. Base64Decode
    • A utility function for decoding a base64 value to text (not binary)
  17. File Fields
    • Getting an item's file field contents.
  18. Using Ranges for Easy List Interaction
    • Quick access to the first or last element in a list.
Recent Comments
Daniel Nolan
Friday, 22 June 2018 10:10 AM
Paul Thompson
Yea! I can't explain how frustrated I've been trying to learn ModScript. I find the syntax to be obscure and counter-intuitive, ... Read More
Wednesday, 23 May 2018 4:04 AM
Continue reading
2408 Hits

SBM ModScript, Part 10 - Regular Expressions

ModScript has the ability to execute regular expressions on strings. The interface for this is the Regex class. In the following example, we create a regular expression that will match any string that starts with "t" (the default options make this case-insensitive). We will then read a full list of users and fill a Vector with users whose loginid starts with "t". Finally, we iterate our Vector and write the users we found to the output stream.

def AppRecord::GetLogin() {
	return this.GetFieldValue("LOGINID").to_string();

var users = Ext.CreateAppRecordList( Ext.TableId("TS_USERS") );

var regex = Regex();
regex.Compile( "^t" );

var out = [];
filter( users, bind( fun( iuser, innerRegex ){ return innerRegex.Matches( user.GetLogin() ); }, _, regex ), back_inserter( out ) );

for_each( out, fun( user ){ Ext.WriteStream( user.GetLogin() ); } );

Step by step:

  • The first thing should look familiar, we talked about adding methods to an existing class in Part 3. In this case, we are making a function that makes it easy to pull the LOGINID out of the User, getting it as a string. 
  • The call to Ext.CreateAppRecordList(), passing in the return value of a call to Ext.TableId(), should be pretty familiar from previous examples. In this case, we are building a list which will let us read rows from the Users table. Then, we call the AppRecordList.Read() method to read the entire Users table (this might not be a great idea on systems with lots of users, but it works well in my simple example). 
  • We create a Regex() and compile it with a simple "starts with t" rule. Since we pass no options into Regex.Compile(), we get the default, which is case-insensitive.
  • We create an output Vector to hold the records that match our regular expression.
  • We invoke the "filter" algorithm. In Part 5 we talked about algorithms, including filter, bind, and back_inserter.
    • filter() - Loops through the range for container "users", invokes the function passed in, for each object where the function returns true, it invokes the second function.
    • users - this is the container to iterate.
    • bind( fun( user, innerRegex ){ return innerRegex.Matches( user.GetLogin() ); }, _, regex ) - bind returns a function for filter to invoke when iterating the users container
      • When the returned function is invoked by the "filter" algorithm, passing in a user from the users container, that value will be passed to the function as the first parameter, which is indicated by the underscore in the call to bind()
      • Also, bind will pass our regex object to the inner function as the second parameter, indicated by the "regex" after the "_" passed to bind.
      • Finally, the inner function will use the regex to indicate to the "filter" call whether this user matches our regular expression.
    • back_inserter( out ) - Adds the matched values to the "out" Vector
      • When the filter function finds a match, it invokes this function, which will append the user object onto the "out" Vector.
  • We assume you want to do something with the filtered list of users. In this case, I invoke the "for_each" algorithm, which will invoke my lambda function on each item in the Vector. In this case, it will print out the matching users' LOGINIDs.


Regular Expressions With Groups

Above, we saw a simple regular expression and a simple call to Regex.Matches(). However, we can also use more complex regular expressions, including group capture.

var regex = Regex();
regex.Compile( "(\\d+)(\\w+)" );

regex.Matches( "123abc" );
for ( var i = 0; i < regex.GroupCount(); ++i ) {
	Ext.WriteStream( regex.GroupVal(i) );

In this example, we have a regular expression with two groups. First, we expect 1 or more digits, followed by 1 or more word-characters. When we invoke Regex.Match() on a string, the Regex object will remember the groups that it matched, and you can access them via Regex.GroupCount() and Regex.GroupVal(). Regex.GroupVal( 0 ) will always be the text matched by the entire expression. After that, the rest of the groups will be indexed in the order they were captured in the string. The above example gives the output:



Regular Expressions: MatchesAgain

After calling Regex.Matches(), you can continue finding matches by invoking Regex.MatchesAgain(). Below, we'll print out each letter in the string, one by one. The regular expression will match any non-digit (\d), non-non-word (\W) character. We do not need grouping parens because the Regex.GroupVal( 0 ) call will always give us the full string that was matched by the regular expression. One thing to note, ChaiScript does not give us a do-while loop, so instead you'll see a while(true)-if-break loop, which is exactly the same paradigm.

var regex = Regex();
regex.Compile( "[^\\d\\W]" );

if ( regex.Matches( "123abc" ) ) {
  while (true) {
	Ext.WriteStream( regex.GroupVal( 0 ) );
	if ( !regex.MatchesAgain() ) { break; }


Regular Expressions: ReplaceAll

Finally, the ModScript Regex class has the ability to use regular expression matching to replace values in strings, returning a modified string with all matches replaced. Also, you can use $ notation to use the matched value, or a matching group number, in the replacement: $0 is the entire matched value, $1 would be the first captured group in the match, etc). 

var regex = Regex();
regex.Compile( "[^\\d\\W]" );

Ext.WriteStream( regex.ReplaceAll( "123abc", "(\$0)" ) );




What happened? I replaced each matching value, in this case the a, b, and c, with a value of the matched text wrapped with parenthesis. As such, "a" became "(a)", etc. You do not need to use groups in the replacement value, you can replace each letter with "D" if you wish. Keep in mind that if you are not trying to use a dollar group-identifier in the replacement string, you will want to escape the dollar symbol with a double backslash \\.


  • It is important to remember that most regular expressions have backslashes in them. ChaiScript uses backslash in string literals to identify special characters like newline: \n and tab: \t. As such, all backslashes that are intended for the regular expression need to be double-backslash \\.
  • As the dollar $ symbol is important in regular expressions, it is important to remember that it also means something in ChaiScript. If ChaiScript finds a ${...} in the string, it will try to invoke the value inside the curly braces as if it were string-injected-script. This could be pretty messy if you accidentally mixed it with a regular expression. It is wise to ChaiScript-escape the $ in the string with a single backslash. If you also are trying to regular-expression-escape the dollar, you may need \\\$.
  • The Regex.Compile() function takes an optional second parameter, which is used to indicate options. The default is a case-insensitive, single-line regular expression. To shut off case-insensitive but keep the other options, pass 0. To set the options you like, pass the options to the second parameter, connected with the ChaiScript bitwise-or operator | . 
    • RegexOptionBitsConstants.IGNORECASE
    • RegexOptionBitsConstants.MULTILINE
    • RegexOptionBitsConstants.DOT_MATCHES_ALL


SBM ModScript - Table of Contents

Recent Comments
Paul Thompson
Don: What RE engine does modscript use (i.e. PCRE, Oniguruma, Boost etc). Basically I would like to know the capabilities (non-gr... Read More
Monday, 17 June 2019 1:01 AM
Paul Thompson
Don: Can you confirm that POSIX character classes are supported? I'm having trouble with:var regex = Regex() ; var str_Test = st... Read More
Saturday, 21 September 2019 5:05 AM
Don Inghram
We use PCRE2.
Monday, 17 June 2019 4:04 PM
Continue reading
1554 Hits

SBM ModScript, Part 9 - SQL Queries

In ModScript, we can read queries from the SBM database. One way to do this is to read items from a table, where each item is based on that specific table's schema. This is what the AppRecord, VarRecord, and ProjectBasedRecord classes (and child classes) are for. When you create one with Ext.CreateAppRecord(), Ext.CreateVarRecord(), or Ext.CreateProjectBasedRecord(), you always pass in the table ID that the record will be based on, so for the life of that object, it will be associated with that table and its schema. Also, we have the AppRecordList (and child classes), which is created with Ext.CreateAppRecordList(), and which also will be bound to a specific table ID on creation. Thus, all items in the AppRecordList will be objects of that tables' type.

All these classes can be used to read records from the SBM table for which the class is bound, using the following functions:

  • AppRecord.Read()
    • Read an item by TS_ID or name.
  • AppRecord.ReadByColumn()
    • Read an item by a column from that table's schema
  • AppRecord.ReadByColumnAndColumn()
    • Use two column values to identify the item to read.
  • AppRecord.ReadWithWhere()
    • Read the item with any SQL where clause. This function can use SQL bind parameters.
  • AppRecord.ReadByUUID()
    • Read an item by TS_UUID
  • AppRecordList.Read()
    • Read an entire table, this could potentially use a lot of resources.
  • AppRecordList.ReadByColumn()
    • Read a list of items by a column from that table's schema
  • AppRecordList.ReadByColumnAndColumn()
    • Use two column values to identify the items to read.
  • AppRecordList.ReadWithWhere()
    • Read the list of items with any SQL where clause. This function can use SQL bind parameters.

When using the ReadWithWhere() functions, I'd encourage the usage of SQL bind parameters. When trying to build SQL using dynamic values, you need to worry that the values you are pulling from fields or other sources could have embedded single-quotes ( ticks: ' ), which can break the SQL. These values would need to be encoded if added directly to the SQL query. However, if instead you put a question mark in the SQL, you can bind the value to it using a Vector of bind values. These bind values do not need to have the ticks encoded, which simplifies your script. In Part 2 of this series, you saw me use SQL bind parameters:

    [ Pair(DBTypeConstants.INTEGER, relational.GetId()),
      Pair(DBTypeConstants.INTEGER, Shell.Item().GetId()) ] );

Each entry in the Vector is a Pair, with the first value being the data type for the value, and the second value being the value I am binding to my SQL. 


AppDb Queries

ModScript also allows for SQL queries that are not tied directly to tables. The Shell.Db() method returns an AppDb object which points to the current SBM AE schema. It has the following functions which allow for free SQL to be executed, (each take an optional Vector of SQL bind parameters):

  • AppDb.ReadIntWithSQL()
    • Returns a single integer read from the database.
  • AppDb.ReadTextWithSQL()
    • Returns a single string read from the database.
  • AppDb.ReadIntegersWithSQL()
    • Fills a Vector with integers read from the database.
  • AppDb.ReadIntegerPairsWithSQL()
    • Fills a Vector with Pairs of integers read from the database.
  • AppDb.ReadTextValsWithSQL()
    • Fills a Vector with strings read from the database.

With these, you can execute any SQL you want as long as the output of the SQL can be bound to as a single int, varchar, a list of ints, list of varchars, or a list of pairs of ints. In 11.4, we added AppDb.ReadDynaSQL(), which allows you to specify any number of columns, with their column types, which makes you truly free to execute any SQL you want.


SQL Performance

Keep in mind that queries can be slow. In SBM, we try to identify any slow queries built into our product, and we add database indexes to mitigate the performance slowdown. However, we can't do this for queries that you add, so you will probably want to watch for places where you should add your own indexes to the database to help speed up your queries.


SBM ModScript - Table of Contents

Continue reading
1613 Hits

Recent Tweets