MagnaDB Framework for .NET

1. Overview



A simple ORM to make things simpler

MagnaDB is a fast and reliable ORM that makes you worry less about databases and lets you code peacefully.
This framework packs Code-First capabilities, as well as Database-First easy integration. It'll go with you, the path you decide to take. No need for complicated migrations, nor strict database access/modification; you do you; just make sure your models and tables match and everything will be just fine.

You're on the No-SQL train? This is for you.
You still love SQL like the first time? This is for you.

MagnaDB is designed to be a Hybrid ORM, letting you manipulate databases objects directly from the code, but also letting the window open databases commands to get in. All this while keeping you safe.

2. Getting Started

In order to get started we first need to install MagnaDB to one of our projects.

For the present example let's create a new Console Application Project for .NET Framework. Let's name it TestMagnaDB (or whatever you wish to name it. It's up to you).

If you're using Visual Studio, or Visual Studio Code (with the NuGet Package Manager extension) you just need to execute the following command in the NuGet Package Manager Console.

PM> Install-Package MagnaDB.SQLite

This command will download the latest version of MagnaDB for SQLite (note: MagnaDB is available for SQL Server and MySQL; if you have any of these DB engines available you're free to try MagnaDB in any of them. learn more).

Please make sure your project's target framework is ≥ .NET Framework 4.6.1

3. Table Models

After you have successfully installed MagnaDB to your project, the next step is to create some models.
We'll start with a Table Model.

A TableModel is a class that resembles a Table definition in a database, and objects from this class are mapped to a row from this table in the database through their properties.

Let's create a Cat model (you can do this by creating a new Class file in your project; name it Cat); it will have the following properties: Id, Name, Race, DateOfBirth.

It would look something like this:

using System;
using MagnaDB.SQLite;

namespace TestMagnaDB
{
    public class Cat
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public string Breed { get; set; }
        public DateTime DateOfBirth { get; set; }
    }
}

The previous example shows a minimally defined model. But, it's not a MagnaDB Table model. In order to make it a table model, Cat must inherit the MagnaDB.TableModel⟨T⟩ abstract class and define the following abstract properties implementation: ConnectionString, TableName, Key.

using System;
using MagnaDB.SQLite;

namespace TestMagnaDB
{
    public class Cat : TableModel<Cat>
    {
        protected override string ConnectionString => "Data Source=testmagna.db;New=True;Version=3;";
        protected override string TableName => "Cats";
        protected override MagnaKey Key => this.MakeKey(cat => cat.Id);

        [Identity]
        public long Id { get; set; }
        public string Name { get; set; }
        public string Breed { get; set; }
        public DateTime DateOfBirth { get; set; }
    }
}

  • TableModel<T>: this class must be inherited when creating a table model and contains all methods and functions concerning them. The implementing class must be specified as the type parameter when defining the inheritance (as seen in the example above).
  • ConnectionString: this propery must return a string containing the properties connection string (formats may vary from one DB Engine to another, please see the DB Engines) for more information.
  • TableName: this propery must return a string containing the name of the database table which this class will be mapped to.
  • Key: this propery must return a MagnaKey object containg a Key/Value combination of the properties that compose the primary key for the class/table (the use of the extension method MakeKey() is preferred, and should provide an easier way to specify all columns composing the primary key).
  • IdentityAttribute [Identity]: this attribute indicates that the property it is decorating will be used as an Identity or Auto Increment column. Learn more

Now that we have correctly defined our model, let's use it in our Main Function in the Program.cs file.

using System;

namespace TestMagnaDB
{
    class Program
    {
        static void Main(string[] args)
        {
            // First things, first, let's create our Cat Table
            // If a table already exists in the database, we'll
            // have an exception thrown, so we'll use try/catch
            try
            {
                Cat.CreateTable();
            }
            catch
            {
                // The table already exists.
            }
            
            // Next, we'll create a new Cat object
            Cat milo = new Cat()
            {
                Name = "Milo",
                Breed = "Persian",
                DateOfBirth = new DateTime(2019, 11, 7)
            };

            // Let's insert Milo to the DB
            milo.Insert();

            // After a TableModel object is successfully inserted to the database
            // it will have its Identity decorated property (if any) automatically updated
            Console.WriteLine("{0}, the {1} cat's Id is {2}", milo.Name, milo.Breed, milo.Id);

            // Turns out Milo's not a Persian, but an American Shorthair
            // We'll fix this and update Milo's breed in the database
            milo.Breed = "American Shorthair";
            milo.Update();

            // If we want to retrieve Milo's information from the database
            // we can do so by using the Get() static method, and providing Milo's Id
            // as an argument. This works for classes with Identity decorated properties.
            Cat miloFromTheDB = Cat.Get(milo.Id);

            Console.WriteLine("This kitten's name is {0}", miloFromTheDB.Name);
            Console.WriteLine("It's a(n) {0}", miloFromTheDB.Breed);
            Console.WriteLine("It was born on {0:MMM dd yyyy}", miloFromTheDB.DateOfBirth);
            Console.WriteLine("{0}Press any key to continue...", Environment.NewLine);
            Console.ReadKey();
        }
    }
}

4. View Models

MagnaDB View Models are models that are only used to retrieve information from the database, meaning they are SELECT Only.

Even though Views are created from existing tables (in engines like SQL Server, or MySql), which means they derive from Tables; in MagnaDB, TableModel is a ViewModel's subclass (which is useful to know), and therefore all operations done with ViewModel can be done with TableModel.

ViewModels DO NOT have to point to a VIEW in order to work.
They can perfectly point at a Table, and query one or more of its columns with no issues.

Let's create a new Class file in our project which we previously created (sections 2., 3.) which we'll name CatView.cs

using System;
using MagnaDB.SQLite;

namespace TestMagnaDB
{
    public class CatView : ViewModel<CatView>
    {
        protected override string ConnectionString => "Data Source=testmagna.db;New=True;Version=3;";
        protected override string TableName => "Cats";
        protected override MagnaKey Key => this.MakeKey(cat => cat.Id);

        [Identity]
        public long Id { get; set; }
        public string Name { get; set; }
        public string Breed { get; set; }
    }
}

The Cats table (which we previously created in section 3.) has four defined columns, which are Id, Name, Breed, and DateOfBirth; notice that in this example we did not include the DateOfBirth column (just for demonstrational purposes).

Let's add some code to our Program class' Main function.
            ...
            // We'll retrieve all cats from the database using the ToList() static
            // method in our CatView class; this can also be done by using table
            // models (like the Cat class we previously created), but we're
            // using the CatView class just for demonstration.
            var aLotOfMilos = CatView.ToList();

            foreach(var item in aLotOfMilos)
            {
                Console.WriteLine("Id: {0}, Name: {1}, Breed: {2}", item.Id, item.Name, item.Breed);
            }

            Console.WriteLine("{0}Press any key to continue...", Environment.NewLine);
            Console.ReadKey();
        }
    }
}

MagnaDB ViewModels are useful for situations where you wish not to query sensitive information from a database (such as password hashes or phone numbers) while querying just the information you need and not all the columns in the table (which will happen when using a table model, mapped to all the columns in the database table).

5. Methods

**DISCLAIMER: The documentation provided for this section contains information for the different MagnaDB classes, properties, methods, events, that are considered to be relevant for its use, and not the whole project.**

1. ViewModel<T> class

This class must be inherited by view model classes and have its type parameter specified as the implementing class in order to work properly (see section 4.).

Methods

Method Description
CreateActiveTransaction() static async Uses this class' connection string property to create an active transaction to the database (which needs to be commited/backrolled, and disposed of after being used).
Returns the newly created transaction.
Will throw an exception if the connection to the database fails.
CreateOpenConnection() static async Uses this class' connection string property to create an open connection. to the database (which needs to be closed/disposed of after being used).
Returns the newly created database connection.
Will throw an exception if the connection to the database fails.
Get() static async Finds and retrieves an object in the database matching the argument-provided key.
Returns true if a table row is successfully inserted. Otherwise, false.
This Methods triggers the GetSucceeded event when a matching result is found.
This Methods triggers the GetFailed event when a matching entity is not found.
Will throw an exception if the database connection fails or if the class definition doesn't correctly match to the table's definition in the database.
GetKey() Returns the object's corresponding Key.
GetTableName() static Returns this object's class' table name.
GetTableName() static Returns this object's class' table name.
LoadRelationships() static async Retrieves and assigns for an object or collection all specified foreign relations see attribute.
TableToIEnumerable() static async Maps a DataTable object's columns to a set of objects' properties and returns an IEnumerable collection with the resulting objects.
Will throw an exception if the database connection fails.
ToDataTable() static async Performs a SELECT to the Database selecting this class' properties that match to columns of the database table. Only columns decorated with the [DataDisplayable] will be selected by default (this behavior can vary).SQL Clauses may be included as arguments.
A DataTable object with the results will be returned.
Will throw an exception if the connection to the database fails, or if the class' definition doesn't correctly match that of the database table.
ToIEnumerable() static async Performs a SELECT to the Database selecting this class' properties that match to columns of the database table. SQL Clauses may be included as arguments.
An IEnumerable<T> object with the results will be returned.
Will throw an exception if the connection to the database fails, or if the class' definition doesn't correctly match that of the database table.
ToList() static async Performs a SELECT to the Database selecting this class' properties that match to columns of the database table. SQL Clauses may be included as arguments.
A List<T> object with the results will be returned.
Will throw an exception if the connection to the database fails, or if the class' definition doesn't correctly match that of the database table.

Properties

Property Description
ConnectionString This property returns this class' specific connection string.
Key Returns this object's corresponding Key.
TableName Returns the name of the table this class is mapped to in the database.

Events

Event Description
GetFailed This event is raised when the Get method doesn't retrieve an entity from the database or runs into an exception of some sort.
GetSucceeded This event is raised when the Get method succeeds in retrieving an entity from the DB.
SelectFailed This event is raised when the ToList, ToIEnumerable, ToDataTable methods don't retrieve an entity from the DB or run into an exception of some sort.
SelectSucceeded This event is raised when the ToList or ToIEnumerable, ToDataTable methods properly retrieve an Entity from the DB.

2. TableModel<T> class

This class must be inherited by model classes and have its type parameter specified as the implementing class in order to work properly (see section 3.).

Methods

Method Description
CreateTable() static Uses this class' properties to create a table in the database specified by the class' connection string or the argument-provided connection/transaction.
Returns true if the table is sucessfully created. Otherwise, false.
Will throw an Exception if the table already exists, or the connection to the database fails.
Delete() async Deletes the database row with a matching key to the invoking object.
Returns true if the matching table row is deleted. Otherwise, false.
This Methods triggers the BeforeDelete before executing the DELETE command.
This Methods triggers the DeleteSucceeded when a database row is successfully deleted.
Will throw an exception if the database connection fails or if the provided Key property is not valid.
GroupInsert() static async Inserts an IEnumerable<T> collection of entities to the the database in a single command. This is specially useful when inserting large GroupInsert of entities.
Returns true if all entities are successfully inserted. Otherwise, false.
Will throw an exception if the database connection fails.
Insert() async Inserts a new row to the class' specified table by mapping the invoking object's properties to columns in the table.
Returns true if a table row is successfully inserted. Otherwise, false.
This Methods triggers the BeforeInsert before executing the INSERT command.
This Methods triggers the InsertSucceeded when a database row is successfully inserted.
Will throw an exception if the database connection fails or if the class definition doesn't correctly match to the table's definition in the database.
IsDuplicated() async Verifies if the object is a duplicated entry by evaluating properties decorated with the [DuplicationColumn] attribute.
Returns true if a duplicate is found. Otherwise, false.
Will throw an exception if the database connection fails.
Update() async Updates the database row matching this object's Key property using each one of its non-key properties.
Returns true if a database row is successfully updated. Otherwise, false.
Will throw an exception if the database connection fails.

Events

Event Description
BeforeDelete This event is raised before the Delete method is executed.
BeforeInsert This event is raised before the Insert method is executed.
BeforeUpdate This event is raised before the Update method is executed.
DeleteFailed This event is raised when the Delete method fails to delete a row in the database.
DeleteSucceeded This event is raised when the Delete method successfully deletes this entity.
InsertFailed This event is raised when the Insert method fails to insert an entity to a table.
InsertSucceeded This event is raised when the Insert method successfully inserts this entity.
UpdateFailed This event is raised when the Update method fails to update a row in the database table.
UpdateSucceeded This event is raised when the Update method successfully updates this entity.

3. QueryMethods static class

This class contains methods that are used to interact with the database through SQL commands.

Methods

Method Description
DoQuery() static async Executes a SQL command against the specified database connection.
Returns true if the query is successfully executed. Otherwise, false.
Will throw an exception if the connection to the database fails, or the provided SQL command is not valid.
DoScalar() static async Executes a SQL query against the specified database connection and returns a single value.
Will throw an exception if the connection to the database fails, or the provided SQL query is not valid.
TableMake() static async Executes a SQL query against the specified database connection and returns the query results inside a DataTable object.
Will throw an exception if the connection to the database fails, or the provided SQL query is not valid.

4. SqlGenerator static class

This class contains methods that are used to generate SQL queries, commands, clauses, etc.

Methods

Method Description
GenDelete() static Returns a string with a DELETE statement generated from the provided arguments.
GenFieldsEnumeration() static Returns a string with a list of comma-separated columns/fields.
GenInsert() static Returns a string with a INSERT statement generated from the provided arguments.
GenSelect() static Returns a string with a SELECT statement generated from the provided arguments.
GenSetPairs() static Returns a string with column/value assignments separated by commas.
GenUpdate() static Returns a string with an UPDATE statement generated from the provided arguments.
GenValuesEnumeration() static Returns a string with a list of formatted comma-separated values.
GenWhere() static Returns a string with a WHERE clause that evaluates that the rows' values are equal to the values provided in each key/value pair.
GenWhereDiffered() static Returns a string with a WHERE clause that evaluates that the rows' values are not equal to the values provided in each key/value pair.

6. Attributes

The following attributes let you manipulate MagnaDB's model's behavior and implement lots of cool functionalities.

All attributes target class properties

We'll be looking at each one of them below.

1. ColumnNameAttribute

This attribute lets you specify the name for a database column mapped to a property, therefore not using the property's identifier, but the one provided in the attribute.

            ...
            [ColumnName("pedigree")]
            public string Breed { get; set; }

2. DataDisplayableAttribute

This attribute lets you mark properties you wish to have included when executing the ToDataTable() static method.

            ...
            [DataDisplayable]
            public string Name { get; set; }

3. DateTimeTypeAttribute

This attribute lets you specify what part of the date this property's associated column in the database should work and be evaluated with (Date, Time, Date and Time).

            ...
            [DateTimeType(DateTimeSpecification.Date)]
            public DateTime DateOfBirth { get; set; }

4. DDLIgnoreAttribute

Properties decorated with this attribute will not be included in internal CREATE, or ALTER statements (like in the case of the CreateTable() method).
This attribute is useful when you're using computed columns that need to be defined from the database, and other scenarios.

            ...
            [DDLIgnore]
            public decimal StockGrowthRatio { get; set; }

5. DMLIgnoreAttribute

Properties decorated with this attribute will not interact with the database in any sort of way.
This attribute is useful when you want to include properties in a model that aren't data-bound, but useful for another functionalities.

            ...
            [DMLIgnore]
            public HttpPostedFileBase UploadedImage { get; set; }

6. DuplicationColumnAttribute

Properties decorated with this attribute will be evaluated when executing the TableModel class' IsDuplicated() method.

This method's constructor lets you specify a duplication index that lets you group different combinations of properties to evaluate for duplication. DuplicationColumn-decorated properties having the same index will be evaluated as a group (if more than one property is specified).
A property may belong to any possible number of different duplication groups by decorating the property more than once.
The duplication index will be 0 by default.
In order to avoid "magic numbers" when using duplication indexes in your models, the use of integer constants is encouraged.

            ...
            public const int PersonalInfoIndex = 1;
            public const int SocialSecurityIndex = 2;
            public const int PassportIndex = 3;

            [DuplicationColumn(PersonalInfoIndex)]
            public string FirstName { get; set; }

            [DuplicationColumn(PersonalInfoIndex)]
            public string LastName { get; set; }

            [DuplicationColumn(PersonalInfoIndex)]
            public DateTime DateOfBirth { get; set; }
            
            [DuplicationColumn(SocialSecurityIndex)]
            public string SocialSecurityNumber { get; set; }
            
            [DuplicationColumn(PassportIndex)]
            public string PassportNumber { get; set; }

            // When invoking the IsDuplicated() method, a parameter for which duplication index
            // will be evaluated can be provided; if -1 (default if none) is provided as
            // an argument, all indexed-groups will be evaluated.

            // FirstName, LastName, and DateOfBirth will be evaluated as a group, meaning that
            // duplicated entities have the same full names and were born on the same day.
            
            // Social Security Number will be evaluated individually.
            // Passport Number will also be evaluated individually.

7. ForeignRelationAttribute

When defining relationships between one model and another, we use the [ForeignRelation] attribute.

A property decorated with this attribute MUST either be a related table or view model, or a collection (List<T> or IEnumerable<T>) of a table or view model.

A string argument must be provided; this argument specifies what properties define the relationship between the two model classes (whether they're one to many, or viceversa).

The string format is: "<{0}.ImplementingClassProperty> = {1}.ForeignClassProperty"

Where:
  • The '<' and '>' signs ARE NOT PLACEHOLDERS and must enclose the implementing class' property.
  • {0} is a shorthand for the implementing class' table name (the tablename can be explicitly specified if so desired).
  • {1} is a shorthand for the foreign class' table name (the tablename can be explicitly specified if so desired).
  • The implementing class property and foreign class property must be of the same type (just as a foreign key relationship in databases would be defined). However, these properties don't need to be a/part of a primary key in order to work, nor does the existence of primary/foreign keys in the database affect the [ForeignRelation] attribute's behavior.
      ...
      public class Cat : TableModel<Cat>
      {
          protected override string ConnectionString => "Data Source=testmagna.db;New=True;Version=3;";
          protected override string TableName => "Cats";
          protected override MagnaKey Key => this.MakeKey(cat => cat.Id);
  
          [Identity]
          public long Id { get; set; }
          public string Name { get; set; }
          public string Breed { get; set; }
          public DateTime DateOfBirth { get; set; }
      
          [ForeignRelation("<{0}.Id> = {1}.PatientId")]
          public List<Checkup> Checkups { get; set; }
      }

      public class Checkup : TableModel<Checkup>
      {
          protected override string ConnectionString => "Data Source=testmagna.db;New=True;Version=3;";
          protected override string TableName => "Checkups";
          protected override MagnaKey Key => this.MakeKey(cat => cat.Id);
  
          [Identity]
          public long Id { get; set; }
          public DateTime CheckupDate { get; set; }
          public string Prescription { get; set; }

          public long PatientId { get; set; }

          [ForeignRelation("<{0}.PatientId> = {1}.Id")]
          public Cat Patient { get; set; }
      }

IMPORTANT: When querying MagnaDB models' relations, avoid circular relations (Example: A Customer model has a foreign relation to sales for each one of its purchases, and a Sale has a relation to the customer who made the purchase. If you queried all sales for a customer, you SHOULDN'T QUERY the customer for the Sales, because it would cause an infinite loop on your application).

8. IdentityAttribute

A property decorated with this attribute will behave as an Identity specificied column, meaning that its value will be auto-generated for each row, and that it'll follow a sequence (varies from one DBMS to another).

When an object with a Identity-decorated property is successfully inserted to the database (using the Insert() method) it's Identity property will automatically be updated to the value generated in the database at insertion.

When invoking the Get() method, if a numeric Id is provided, the class' Identity property will be used as the search criteria (recommended).

Properties decorated with this attribute must be of the supported Identity/AUTO_INCREMENT type specification types for each database engines (see section 9.).

ONLY ONE [Identity] ATTRIBUTE SHOULD EXIST WITHIN A CLASS TO AVOID MALFUNCTIONS.

            ...
            [Identity]
            public long Id { get; set; }

9. InsertIgnoreAttribute

Properties decorated with this attribute will not be included in commands when using the Insert() method.

            ...
            [InsertIgnore]
            public DateTime TimeStamp { get; set; }

10. SelectIgnoreAttribute

Properties decorated with this attribute will not be included when invoking methods that retrieve data from the database (such as Get() and ToList())

            ...
            [SelectIgnore]
            public string SocialSecurityNumber { get; set; }

11. UpdateIgnoreAttribute

Properties decorated with this attribute will not be included in commands when using the Update() method.

            ...
            [UpdateIgnore]
            public string SongId { get; set; }

7. Events

MagnaDB events provide functionalities similar to database triggers.

Whether you're trying a only-code approach for your application, or your database services are limited, and don't provide you with database triggers; events are a perfect workaround. And, who knows? They might even provide some extra functionalities.

The events in MagnaDB are the following:

For ViewModel<T>

  • GetFailed
  • GetSucceeded
  • SelectFailed
  • SelectSucceeded

For TableModel<T>
  • GetFailed (inherited)
  • GetSucceeded (inherited)
  • SelectFailed (inherited)
  • SelectSucceeded (inherited)
  • BeforeInsert
  • InsertFailed
  • InsertSucceeded
  • BeforeUpdate
  • UpdateFailed
  • UpdateSucceeded
  • BeforeDelete
  • DeleteFailed
  • DeleteSucceeded

Here's a quick example on how to use events in MagnaDB.

using System;
using MagnaDB.SQLite;

namespace TestMagnaDB
{
    public class User : TableModel<User>
    {
        protected override string ConnectionString => "Data Source=testmagna.db;New=True;Version=3;";
        protected override string TableName => "Users";
        protected override MagnaKey Key => this.MakeKey(user => user.Id);

        [Identity]
        public long Id { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
        public string Username { get; set; }
        public string PasswordHash { get; set; }
        public bool EmailIsConfirmed { get; set; }

        public User()
        {
            BeforeInsert += User_BeforeInsert;
            InsertSucceeded += User_InsertSucceeded;
        }

        // The sender parameter receives the invoking object for Insert(),
        // Update(), and Delete() methods. For methods retrieving data the
        // object(s) resulting from the queries are received as the sender
        // parameter instead.

        // The MagnaEventArgs contains properties relating to the database
        // connection including the transaction (if any), the connection
        // (if any), and/or the connection string (if any), the triggering 
        // method's command was executed against.
        public void User_BeforeInsert(object sender, MagnaEventArgs e)
        {
            PasswordHash = AnExistingClassYouMayHave.HashFunctionThatIsSomewhereElse(PasswordHash);
        }

        // Notice how this event handler is implemented asynchronously
        // In some contexts this can mean performance optimization for
        // your application, so don't be afraid to try it.
        public async void User_InsertSucceeded(object sender, MagnaEventArgs e)
        {
            await AnExistingClassYouMightAlsoHave.SendEmailConfirmationAsync(Id);
        }
    }
}

8. Exceptions

These are the existing exception types in MagnaDB.

Exception Description
DbConnectionException Thrown when an error occurs while connecting to the database or database Server.
DisparityException This exception is thrown when a SqlGenerator method is being called with incorrect key/value pairs.
InvalidModelException Thrown when the existing model does not correctly match with the database table/view.
InvalidTableException Thrown when a Table (or other queryable database object) doesn't exist this exception is raised

9. Database Engines

MagnaDB is currently available for the following DB Engines:

  • SQL Server
  • MySQL
  • SQLite

There are minor things that vary from one platform to another.

Database Connections in SqlServer

In order to perform database operations, MagnaDB.SQLite works with the System.Data.SqlClient package. Connections valid for this package are supported by MagnaDB.SqlServer.

Methods that perform database operations in MagnaDB.SqlServer receive SqlConnection (must be open), or SqlTransaction (must be active) as parameters if you wish to perform multiple operations on a single connection/transaction; just make sure to commit/rollback/dispose all connection related objects after use.

Database Connections in MySQL

In order to perform database operations, MagnaDB.MySQL works with the MySql.Data package. Connections valid for this package are supported by MagnaDB.MySQL.

Methods that perform database operations in MagnaDB.MySql receive MySqlConnection (must be open), or SqlTransaction (must be active) as parameters if you wish to perform multiple operations on a single connection/transaction; just make sure to commit/rollback/dispose all connection related objects after use.

Database Connections in SQLite

In order to perform database operations, MagnaDB.SQLite works with the System.Data.SQLite package. Connections valid for this package are supported by MagnaDB.SQLite.

Methods that perform database operations in MagnaDB.SQLite receive SQLiteConnection (must be open), or SQLiteTransaction (must be active) as parameters if you wish to perform multiple operations on a single connection/transaction; just make sure to commit/rollback/dispose all connection related objects after use.

Integer support in SQLite

For SQLite, you could get type affinity errors when working with byte, int, or anything that's not long (Int64), so beware of how some .NET types may not work with SQLite.