Database Structure¶
There are four parts to the database structure definition:
Tables - a dictionary mapping the table name (string) to a list of field names (also strings)
Field Types - a dictionary mapping the field name (string) to a string for the type to be used for that field in all tables.
Default Type - a string representing the default type for a field if it is not specified above. (By default,
'TEXT')Primary Keys - A list of string field names that should be treated as primary keys. (By default,
['id'])
There are two places to define the database structure.
Using
SQLiteDB, you can directly modify thetables,field_types,default_typeandprimary_keysand then callupdate_database_structure()default_typeandprimary_keyscan also be passed in in the constructor.
Using
MetroDB, the definition is loaded in via theyamlfile, which should be a dictionary at the top level, with at the very least, the tables definition with the keytables. The other parts are optional, and can be loaded with the keystypes,default_type, andprimary_keys.You can also use the more verbose
field_typesfortypes
SQL Limitations¶
The functionality implemented here is only a small subset of what can be done with SQL. Here are a couple of key limitations.
All fields with the same name have the same type
The only SQL constraint implemented thus far is
PRIMARY KEY
Datatypes¶
Every field has an associated “type”, which is really TWO types, the Python type that is used in the Python code, and the SQL type that is used in the database. The standard available types are:
Python Type |
SQL Type |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
When you want to use a Python type in SQL, it will automatically be adapted into its corresponding SQL type. When you read a value from the SQL, it will automatically be converted into its corresponding Python type. (This language is important in the Custom Data Types section below.)
When specifying a type in the database structure, use the Python type.
Example¶
from metro_db import SQLiteDB
db = SQLiteDB('type_demo')
db.tables['people'] = ['name', 'age', 'grade', 'present']
db.field_types['age'] = 'int'
db.field_types['grade'] = 'float'
db.field_types['present'] = 'bool'
db.update_database_structure()
db.insert('people', {'name': 'David', 'age': 21, 'grade': 98.6, 'present': True})
db.insert('people', {'name': 1, 'age': 1, 'grade': 1, 'present': 1})
for row in db.query('SELECT * FROM people'):
for key, value in dict(row).items():
print(f'{key:8s}: {repr(value):>8s} ({type(value)})')
The output will be
name : 'David' (<class 'str'>)
age : 21 (<class 'int'>)
grade : 98.6 (<class 'float'>)
present : True (<class 'bool'>)
name : '1' (<class 'str'>)
age : 1 (<class 'int'>)
grade : 1.0 (<class 'float'>)
present : True (<class 'bool'>)
Note that the values read from the database have the specified Python type (where possible), regardless of what type was inserted (by design).
Custom Data Types¶
You can also set up the database to have custom data types, which requires four things:
A name for the type (a string)
The Python type (notably, not the string representing the type, but the type itself)
An adapter function for translating the Python type into the SQL type.
A converter function for translating the SQL type to the Python type.
For example, bool is not natively supported by sqlite3. Instead it is implemented with the following snippet.
db.register_custom_type('bool', bool, int, lambda v: bool(v))
The name is the string
'bool'The type is the Python type for
boolTo adapt a
boolto an integer for the SQL, we use the built-inintfunctionTo convert an integer to a
bool, we use the built-inboolfunction.
Additionally, you can register custom IntEnum to be stored as integers in the SQL with register_custom_enum:
class Direction(IntEnum):
NORTH = 1
SOUTH = 2
WEST = 3
EAST = 4
db.register_custom_enum(Direction)
The name and adapter/converter functions are generated automatically.