2026-02-23 18:38:03 -06:00
extends Resource
class_name SQLiteObject
## A Data Object representative of data to store in [SQLite] database.
##
## [SQLiteObject] is the core class for GDataORM. It handles the grunt work
## of defining what table structure is and any special flags that are needed
## for SQLite.[br][br]
##
## [b]Example:[/b]
## [codeblock]
## extends SQLiteObject
## class_name Account
##
## var id: int
## var username: String
## var password: String
## var address: Address
##
## static func _setup() -> void:
## set_table_name(Account, "accounts")
## set_column_flags(Account, "id", Flags.PRIMARY_KEY | Flags.AUTO_INCREMENT | Flags.NOT_NULL)
## set_column_flags(Account, "username", Flags.NOT_NULL)
## set_column_flags(Account, "password", Flags.NOT_NULL)
## [/codeblock]
## The supported types of [SQLiteObject]
enum DataType {
## A [bool] Value
BOOL ,
## An [int] Value
INT ,
## A [float] Value
REAL ,
## A Variable Length [String] Value
STRING ,
## A [Dictionary] Value
DICTIONARY ,
## An [Array] Value
ARRAY ,
## A value of a built-in Godot DataType, or Object of a Custom Class.
GODOT_DATATYPE ,
## A Fixed-size [String] value, like [PackedStringArray]
CHAR ,
## A Binary value, like [PackedByteArray]
BLOB
}
const _BaseTypes = {
TYPE_BOOL : DataType . BOOL ,
TYPE_INT : DataType . INT ,
TYPE_FLOAT : DataType . REAL ,
TYPE_STRING : DataType . STRING ,
TYPE_DICTIONARY : DataType . DICTIONARY ,
TYPE_ARRAY : DataType . ARRAY ,
}
const _DEFINITION = [
" int " ,
" int " ,
" real " ,
" text " ,
" text " ,
" text " ,
" blob " ,
" char( %d ) " ,
" blob "
]
## SQLite flags used for column definitions.
enum Flags {
## No Flags Associated with this Column
NONE = 1 << 0 ,
## Column must not be Null.
NOT_NULL = 1 << 1 ,
## Column must be Unique
UNIQUE = 1 << 2 ,
## Column has a Default value.
DEFAULT = 1 << 3 ,
## Column is defined as a Primary Key for this table.
PRIMARY_KEY = 1 << 4 ,
## Column is defined as auto-incrementing.
AUTO_INCREMENT = 1 << 5 ,
## Column is a Foreign Key (See [SQLite] about Foreign Keys)
FOREIGN_KEY = 1 << 6 ,
}
class TableDefs :
var columns : Dictionary [ String , Dictionary ] = { }
var types : Dictionary [ String , DataType ] = { }
var klass : GDScript
var table_name : String
static var _tables : Dictionary [ GDScript , TableDefs ] = { }
static var _registry : Dictionary [ String , GDScript ] = { }
var _db : SQLite
## A debugging utility to see what classes have been registered with [SQLiteObject].
## This is printed out to the terminal/output window for easy review.
static func print_registered_classes ( ) - > void :
print ( " SQLiteObject Registered Classes: " )
for klass_name in _registry :
print ( klass_name )
## A debugging utility to see the structure of all the classes registered with [SQLiteObject].
## This is printed out to the terminal/output window for easy review.
static func print_data_structure ( ) - > void :
print ( " SQLite Object Data Structure: " )
print ( " ----------------------------- " )
for klass in _tables :
var table = _tables [ klass ]
print ( " SQLiteObject> %s " % klass . get_global_name ( ) )
print ( " Table Name: %s " % table . table_name )
print ( " COLUMNS: " )
for column in table . columns :
var keys : Array = table . columns [ column ] . keys ( ) . filter ( func ( x ) : return x != " data_type " )
var columns : = [ table . columns [ column ] . data_type ]
columns . append_array ( keys )
print ( " \t %s (DataType. %s ) - SQLite: ( %s ) " % [
column ,
DataType . find_key ( table . types [ column ] ) ,
" , " . join ( columns )
] )
print ( " " )
pass
## This function is called once when setting up the class. This is automatically done with classes
## that are registered as a [DbSet] by the [method Context.setup] static function call.
static func setup ( klass : GDScript ) - > void :
_registry [ klass . get_global_name ( ) ] = klass
var table : TableDefs
if _tables . has ( klass ) :
table = _tables [ klass ]
else :
table = TableDefs . new ( )
table . klass = klass
table . table_name = klass . get_global_name ( )
_tables [ klass ] = table
for prop in klass . get_script_property_list ( ) :
if not prop . usage & PROPERTY_USAGE_SCRIPT_VARIABLE :
continue
if prop . name . begins_with ( " _ " ) :
continue
var def = { }
if _BaseTypes . has ( prop . type ) :
def . data_type = _DEFINITION [ _BaseTypes [ prop . type ] ]
table . types [ prop . name ] = _BaseTypes [ prop . type ]
else :
def . data_type = _DEFINITION [ DataType . GODOT_DATATYPE ]
table . types [ prop . name ] = DataType . GODOT_DATATYPE
table . columns [ prop . name ] = def
klass . _setup ( )
## This is a virtual function that is called when setup() is called. This allows you to
## setup the data class information such as Column Flags, Table Name and Column Types.
static func _setup ( ) - > void :
push_warning ( " No setup has been defined for this class. No special column flags or types will be used. " )
## This function allows you to set SQLite specific flags for columns, when storing the data.
## This function should only be called in [method SQLiteObject._setup] which is part of the
## initialization of the data.[br][br]
## [b]Example:[/b]
## [codeblock]
## static func _setup() -> void:
## # Ensure ID is an Auto-Increment Primary key in the database, that is not allowed to be null.
## set_column_flag(MyDataClass, "id", Flags.PRIMARY_KEY | Flags.AUTO_INCREMENT | Flags.NOT_NULL)
## # Ensure that name is not null in the database, and that it doesn't match any other row of data.
## set_column_flag(MyDataClass, "name", Flags.NOT_NULL | Flags.UNIQUE)
## [/codeblock]
static func set_column_flags ( klass : GDScript , column : String , flags : int , extra_params : Dictionary = { } ) - > void :
assert ( _tables . has ( klass ) , " Setup must be called first, before setting any column flags! " )
assert ( _tables [ klass ] . columns . has ( column ) , " Column has not been defined! Make sure to declare the variable first! " )
var data_type = _tables [ klass ] . types [ column ]
var col_def = _tables [ klass ] . columns [ column ]
if flags & Flags . DEFAULT and not extra_params . has ( " default " ) :
assert ( false , " Attempting to set a default, without defining it in extra parameters! " )
if flags & Flags . AUTO_INCREMENT and not [ DataType . INT , DataType . REAL ] . has ( data_type ) :
assert ( false , " Attempting to set Auto Increment flag on Non-Integer column! " )
if flags & Flags . FOREIGN_KEY :
if not extra_params . has ( " table " ) :
assert ( false , " Attempting to set Foreign Key flag without defining the Table it associates with! " )
if not extra_params . has ( " foreign_key " ) :
assert ( false , " Attempting to set Foreign Key flag without defining the Foreign Key! " )
if flags & Flags . NOT_NULL : col_def . not_null = true
if flags & Flags . UNIQUE : col_def . unique = true
if flags & Flags . DEFAULT : col_def . default = extra_params . default
if flags & Flags . AUTO_INCREMENT : col_def . auto_increment = true
if flags & Flags . PRIMARY_KEY : col_def . primary_key = true
if flags & Flags . FOREIGN_KEY :
col_def . foreign_key = extra_params . foreign_key
col_def . foreign_table = extra_params . table
_tables [ klass ] . columns [ column ] = col_def
## Sets the table name to use in the [SQLite] database for storing/fetching data
## from the database.
static func set_table_name ( klass : GDScript , table_name : String ) - > void :
assert ( _tables . has ( klass ) , " Setup must be called first, before setting the table name! " )
_tables [ klass ] . table_name = table_name if table_name != " " else klass . get_global_name ( )
## Sets the column type of [enum SQLiteObject.DataType] along with any extra parameters needed.[br][br]
## [b][color=red]NOTE:[/color][/b] Only use this function if you know what you are doing. GDataORM
## attempts to match the SQLite data type, with the Godot data type as best as possible.
static func set_column_type ( klass : GDScript , column : String , type : DataType , extra_params : Dictionary = { } ) - > void :
assert ( _tables . has ( klass ) , " Setup must be called first, before setting any column types! " )
assert ( _tables [ klass ] . columns . has ( column ) , " Column has not been defined! Make sure to declare the variable first! " )
if type == DataType . CHAR and not extra_params . has ( " size " ) :
assert ( false , " Attempting to set Column type to CHAR without a size parameter! " )
2026-03-06 21:25:21 -06:00
_tables [ klass ] . types [ column ] = type
_tables [ klass ] . columns [ column ] . data_type = _DEFINITION [ type ] if type != DataType . CHAR else _DEFINITION [ type ] % extra_params . size
2026-02-23 18:38:03 -06:00
## Sets a variable that has been defined in the class, to be ignored, so as to not persist the data in the
## [SQLite] database. The variable must be defined, in order for it to be ignored.[br][br]
## [b][color=red]NOTE:[/color][/b] By default, GDataORM ignore's any variables that start with [code]_[/code] character.
static func ignore_column ( klass : GDScript , column : String ) - > void :
assert ( _tables . has ( klass ) , " Setup must be called first, before ignoring any column types! " )
assert ( _tables [ klass ] . columns . has ( column ) , " Column has not been defined! Make sure to declare the variable first! " )
_tables [ klass ] . types . erase ( column )
_tables [ klass ] . columns . erase ( column )
## Adds a variable that is normally ignored in the class, to not be ignoerd, so that it can persist the data
## in the [SQLite] database. The variable must be defined, in order for this function to succeed.
static func add_column ( klass : GDScript , column : String ) - > void :
assert ( _tables . has ( klass ) , " Setup must be called first, before adding any column types! " )
var props = klass . get_property_list ( )
var res = props . filter ( func ( x ) : return x . name == column )
assert ( res . size ( ) > 0 , " You cannot add a column, that does not have the variable defined for it! " )
var prop = res [ 0 ]
var def = { }
if _BaseTypes . has ( prop . type ) :
def . data_type = _DEFINITION [ _BaseTypes [ prop . type ] ]
_tables [ klass ] . types [ prop . name ] = _BaseTypes [ prop . type ]
else :
def . data_type = _DEFINITION [ DataType . GODOT_DATATYPE ]
_tables [ klass ] . types [ prop . name ] = DataType . GODOT_DATATYPE
_tables [ klass ] . columns [ prop . name ] = def
static func _create_table ( db : SQLite , klass : GDScript , drop_if_exists = false ) - > void :
assert ( _tables . has ( klass ) , " Setup must be called first, before setting any column types! " )
assert ( not _tables [ klass ] . columns . is_empty ( ) , " No columns has been defined, either no variables are defined in the GDScript source, or setup was not called first! " )
if _table_exists ( db , klass ) :
if drop_if_exists :
db . drop_table ( _tables [ klass ] . table_name )
else :
assert ( false , " Table already exists! " )
db . create_table ( _tables [ klass ] . table_name , _tables [ klass ] . columns )
static func _table_exists ( db : SQLite , klass : GDScript ) - > bool :
assert ( _tables . has ( klass ) , " Setup must be called first, before setting any column types! " )
var table : = _tables [ klass ]
db . query_with_bindings ( " SELECT name FROM sqlite_master WHERE type= ' table ' AND name=?; " , [ table . table_name ] )
return not db . query_result . is_empty ( )
static func _has_id ( db : SQLite , klass : GDScript , id : Variant ) - > bool :
var primary_key = _get_primary_key ( klass )
var table : = _tables [ klass ]
if typeof ( id ) == TYPE_STRING :
db . query_with_bindings ( " SELECT ? FROM ? WHERE ?= ' ? ' " , [ primary_key , table . table_name , primary_key , id ] )
else :
db . query_with_bindings ( " SELECT ? FROM ? WHERE ?=?; " , [ primary_key , table . table_name , primary_key , id ] )
return not db . query_result . is_empty ( )
static func _get_primary_key ( klass : GDScript ) - > String :
assert ( _tables . has ( klass ) , " Setup must be called first, before setting any column types! " )
var table : = _tables [ klass ]
var primary_key : String = " "
for column in table . columns :
if table . columns [ column ] . has ( " primary_key " ) :
primary_key = column
break
assert ( primary_key != " " , " No primary key has been defined! " )
return primary_key
static func _populate_object ( table : TableDefs , obj : SQLiteObject , data : Dictionary ) - > void :
var props = obj . get_property_list ( )
for key in data :
if not props . any ( func ( x ) : return x . name == key ) :
continue
var prop = props . filter ( func ( x ) : return x . name == key ) [ 0 ]
if ( table . types [ key ] == DataType . ARRAY or
table . types [ key ] == DataType . DICTIONARY ) :
obj . get ( key ) . assign ( JSON . parse_string ( data [ key ] ) )
elif table . types [ key ] == DataType . GODOT_DATATYPE :
if _registry . has ( prop . class_name ) :
var klass : = _registry [ prop . class_name ]
var cond : = Condition . new ( )
var pk : String = _get_primary_key ( klass )
cond . equal ( pk , bytes_to_var ( data [ key ] ) )
var nobj = _find_one ( obj . _db , klass , cond )
obj . set ( key , nobj )
else :
obj . set ( key , bytes_to_var ( data [ key ] ) )
else :
obj . set ( key , data [ key ] )
static func _find_one ( db : SQLite , klass : GDScript , conditions : Condition ) - > SQLiteObject :
assert ( _tables . has ( klass ) , " Setup must be called first, before setting any column types! " )
var table : = _tables [ klass ]
var res : = db . select_rows ( table . table_name , conditions . to_string ( ) , table . columns . keys ( ) )
if res . is_empty ( ) :
return null
else :
var obj = klass . new ( )
obj . _db = db
_populate_object ( table , obj , res [ 0 ] )
return obj
static func _find_many ( db : SQLite , klass : GDScript , conditions : Condition ) - > Array :
assert ( _tables . has ( klass ) , " Setup must be called first, before setting any column types! " )
var table : = _tables [ klass ]
var objs : Array = [ ]
var res = db . select_rows ( table . table_name , conditions . to_string ( ) , table . columns . keys ( ) )
for data in res :
var obj = klass . new ( )
obj . _db = db
_populate_object ( table , obj , data )
objs . append ( obj )
return objs
static func _all ( db : SQLite , klass : GDScript ) - > Array :
assert ( _tables . has ( klass ) , " Setup must be called first, before setting any column types! " )
var table : = _tables [ klass ]
var objs : Array = [ ]
var res = db . select_rows ( table . table_name , " " , table . columns . keys ( ) )
for data in res :
var obj = klass . new ( )
obj . _db = db
_populate_object ( table , obj , data )
objs . append ( obj )
return objs
## Verify that the [SQLiteObject] exists in the database.
func exists ( ) - > bool :
assert ( _tables . has ( self . get_script ( ) ) , " Setup must be called first, before setting any column types! " )
assert ( _db , " exists(): This instance was not fetched from the database, or has not been added to a DbSet! " )
var table : = _tables [ self . get_script ( ) ]
var primary_key = _get_primary_key ( self . get_script ( ) )
assert ( primary_key != " " , " A Primary Key has not been defined for this class. " )
var res = _db . select_rows ( table . table_name ,
Condition . new ( ) . equal ( primary_key , self . get ( primary_key ) ) . to_string ( ) ,
[ primary_key ] )
return not res . is_empty ( )
## Saves the [SQLiteObject] to the database file.[br][br]
## [b][color=red]NOTE:[/color][/b] Of special note, an object needs to be added to a [DbSet] first through
## [method DbSet.append] for this function to work. [method DbSet.append] will save the object when
## it is first added. This function is mostly for recording updates to the [SQLiteObject] data.
func save ( ) - > void :
assert ( _tables . has ( self . get_script ( ) ) , " Setup must be called first, before setting any column types! " )
assert ( _db , " save(): This instance was not fetched from the database, or has not been added to a DbSet! " )
var table : = _tables [ self . get_script ( ) ]
var primary_key = _get_primary_key ( self . get_script ( ) )
var sql_data = { }
var data : Variant
for key in table . columns . keys ( ) :
data = get ( key )
if ( table . types [ key ] == DataType . ARRAY or
table . types [ key ] == DataType . DICTIONARY
) :
sql_data [ key ] = JSON . stringify ( data )
elif table . types [ key ] == DataType . GODOT_DATATYPE :
if typeof ( data ) == TYPE_OBJECT :
if _registry . has ( data . get_script ( ) . get_global_name ( ) ) :
var pk : = _get_primary_key ( data . get_script ( ) )
var pk_val = data . get ( pk )
sql_data [ key ] = var_to_bytes ( pk_val )
else :
sql_data [ key ] = var_to_bytes ( data )
else :
sql_data [ key ] = var_to_bytes ( data )
else :
sql_data [ key ] = data
if primary_key != " " and exists ( ) :
_db . update_rows ( table . table_name , Condition . new ( ) . equal ( primary_key , get ( primary_key ) ) . to_string ( ) , sql_data )
else :
if primary_key != " " and table . columns [ primary_key ] . auto_increment :
sql_data . erase ( primary_key )
_db . insert_row ( table . table_name , sql_data )
if primary_key != " " and table . columns [ primary_key ] . auto_increment :
var cond : = Condition . new ( ) . equal ( " name " , " %s " % table . table_name )
var res : = _db . select_rows ( " sqlite_sequence " , cond . to_string ( ) , [ " seq " ] )
assert ( not res . is_empty ( ) , " Failed to insert record into %s . " % [ table . table_name ] )
set ( primary_key , res [ 0 ] . seq )
## Removes the [SQLiteObject] from the database. This will fail, if the object was not fetched
## from the database first. You can also use [method DbSet.erase] to remove an object from the
## database.
func delete ( ) - > void :
assert ( _tables . has ( self . get_script ( ) ) , " Setup must be called first, before setting any column types! " )
assert ( _db , " delete(): This instance was not fetched from the database, or has not been added to a DbSet! " )
var table : = _tables [ self . get_script ( ) ]
var primary_key = _get_primary_key ( self . get_script ( ) )
assert ( primary_key != " " , " In order to delete data from the database, it must have a primary key! " )
if not exists ( ) :
push_warning ( " Attempting to delete a record that doesn ' t exist! " )
return
_db . delete_rows ( table . table_name , Condition . new ( ) . equal ( primary_key , get ( primary_key ) ) . to_string ( ) )
func _to_string ( ) - > String :
assert ( _tables . has ( self . get_script ( ) ) , " Setup must be called first, before setting any column types! " )
var table : = _tables [ self . get_script ( ) ]
var primary_key = _get_primary_key ( self . get_script ( ) )
var kname = self . get_script ( ) . get_global_name ( )
if primary_key != " " :
return " < %s : %s : %s > " % [ kname , table . table_name , get ( primary_key ) ]
else :
return " < %s : %s :G- %s > " % [ kname , table . table_name , get_instance_id ( ) ]