Recognize boolean and datetime column in moor

This commit is contained in:
Simon Binder 2019-10-05 21:24:41 +02:00
parent fb7c3c2a9a
commit ed03bff4c2
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
13 changed files with 174 additions and 27 deletions

View File

@ -97,6 +97,15 @@ invalid.
variable can clash with the explicit index, which is why moor forbids
it. Of course, `id IN ? OR title = ?` will work as expected.
## Supported column types
We use [this algorithm](https://www.sqlite.org/datatype3.html#determination_of_column_affinity)
to determine the column type based on the declared type name.
Additionally, columns that have the type name `BOOLEAN` or `DATETIME` will have
`bool` or `DateTime` as their Dart counterpart. Both will be
written as an `INTEGER` column when the table gets created.
## Imports
{{% alert title="Limited support" %}}
> Importing a moor file from another moor file will work as expected.

View File

@ -57,5 +57,10 @@ Make sure that your project depends on moor 2.0 or later. Then
Unfortunately, we can't support IntelliJ and Android Studio yet. Please vote on
[this issue](https://youtrack.jetbrains.com/issue/WEB-41424) to help us here!
As a workaround, you can configure IntellIJ to recognize moor files as sql. Moor-only
features like imports and Dart templates will report errors, but the rest of the
syntax works well. See [this comment](https://github.com/simolus3/moor/issues/150#issuecomment-538582696)
on how to set this up.
If you're looking for support for an other IDE that uses the Dart analysis server,
please create an issue. We can probably make that happen.

View File

@ -2,6 +2,8 @@
- Introduced `isBetween` and `isBetweenValues` methods for comparable expressions (int, double, datetime)
to check values for both an upper and lower bound
- Automatically map `BOOLEAN` and `DATETIME` columns declared in a sql file to the appropriate type
(both used to be `double` before).
## 2.0.0
This is the first major update after the initial release and moor and we have a lot to cover:

View File

@ -176,7 +176,7 @@ class GeneratedBoolColumn extends GeneratedColumn<bool, BoolType>
$customConstraints: $customConstraints, defaultValue: defaultValue);
@override
final String typeName = 'BOOLEAN';
final String typeName = 'INTEGER';
@override
void writeCustomConstraints(StringBuffer into) {

View File

@ -645,16 +645,25 @@ class ConfigTable extends Table with TableInfo<ConfigTable, Config> {
class MytableData extends DataClass implements Insertable<MytableData> {
final int someid;
final String sometext;
MytableData({@required this.someid, this.sometext});
final bool somebool;
final DateTime somedate;
MytableData(
{@required this.someid, this.sometext, this.somebool, this.somedate});
factory MytableData.fromData(Map<String, dynamic> data, GeneratedDatabase db,
{String prefix}) {
final effectivePrefix = prefix ?? '';
final intType = db.typeSystem.forDartType<int>();
final stringType = db.typeSystem.forDartType<String>();
final boolType = db.typeSystem.forDartType<bool>();
final dateTimeType = db.typeSystem.forDartType<DateTime>();
return MytableData(
someid: intType.mapFromDatabaseResponse(data['${effectivePrefix}someid']),
sometext: stringType
.mapFromDatabaseResponse(data['${effectivePrefix}sometext']),
somebool:
boolType.mapFromDatabaseResponse(data['${effectivePrefix}somebool']),
somedate: dateTimeType
.mapFromDatabaseResponse(data['${effectivePrefix}somedate']),
);
}
factory MytableData.fromJson(Map<String, dynamic> json,
@ -662,6 +671,8 @@ class MytableData extends DataClass implements Insertable<MytableData> {
return MytableData(
someid: serializer.fromJson<int>(json['someid']),
sometext: serializer.fromJson<String>(json['sometext']),
somebool: serializer.fromJson<bool>(json['somebool']),
somedate: serializer.fromJson<DateTime>(json['somedate']),
);
}
@override
@ -670,6 +681,8 @@ class MytableData extends DataClass implements Insertable<MytableData> {
return {
'someid': serializer.toJson<int>(someid),
'sometext': serializer.toJson<String>(sometext),
'somebool': serializer.toJson<bool>(somebool),
'somedate': serializer.toJson<DateTime>(somedate),
};
}
@ -681,47 +694,74 @@ class MytableData extends DataClass implements Insertable<MytableData> {
sometext: sometext == null && nullToAbsent
? const Value.absent()
: Value(sometext),
somebool: somebool == null && nullToAbsent
? const Value.absent()
: Value(somebool),
somedate: somedate == null && nullToAbsent
? const Value.absent()
: Value(somedate),
) as T;
}
MytableData copyWith({int someid, String sometext}) => MytableData(
MytableData copyWith(
{int someid, String sometext, bool somebool, DateTime somedate}) =>
MytableData(
someid: someid ?? this.someid,
sometext: sometext ?? this.sometext,
somebool: somebool ?? this.somebool,
somedate: somedate ?? this.somedate,
);
@override
String toString() {
return (StringBuffer('MytableData(')
..write('someid: $someid, ')
..write('sometext: $sometext')
..write('sometext: $sometext, ')
..write('somebool: $somebool, ')
..write('somedate: $somedate')
..write(')'))
.toString();
}
@override
int get hashCode => $mrjf($mrjc(someid.hashCode, sometext.hashCode));
int get hashCode => $mrjf($mrjc(someid.hashCode,
$mrjc(sometext.hashCode, $mrjc(somebool.hashCode, somedate.hashCode))));
@override
bool operator ==(other) =>
identical(this, other) ||
(other is MytableData &&
other.someid == this.someid &&
other.sometext == this.sometext);
other.sometext == this.sometext &&
other.somebool == this.somebool &&
other.somedate == this.somedate);
}
class MytableCompanion extends UpdateCompanion<MytableData> {
final Value<int> someid;
final Value<String> sometext;
final Value<bool> somebool;
final Value<DateTime> somedate;
const MytableCompanion({
this.someid = const Value.absent(),
this.sometext = const Value.absent(),
this.somebool = const Value.absent(),
this.somedate = const Value.absent(),
});
MytableCompanion.insert({
this.someid = const Value.absent(),
this.sometext = const Value.absent(),
this.somebool = const Value.absent(),
this.somedate = const Value.absent(),
});
MytableCompanion copyWith({Value<int> someid, Value<String> sometext}) {
MytableCompanion copyWith(
{Value<int> someid,
Value<String> sometext,
Value<bool> somebool,
Value<DateTime> somedate}) {
return MytableCompanion(
someid: someid ?? this.someid,
sometext: sometext ?? this.sometext,
somebool: somebool ?? this.somebool,
somedate: somedate ?? this.somedate,
);
}
}
@ -746,8 +786,24 @@ class Mytable extends Table with TableInfo<Mytable, MytableData> {
$customConstraints: '');
}
final VerificationMeta _someboolMeta = const VerificationMeta('somebool');
GeneratedBoolColumn _somebool;
GeneratedBoolColumn get somebool => _somebool ??= _constructSomebool();
GeneratedBoolColumn _constructSomebool() {
return GeneratedBoolColumn('somebool', $tableName, true,
$customConstraints: '');
}
final VerificationMeta _somedateMeta = const VerificationMeta('somedate');
GeneratedDateTimeColumn _somedate;
GeneratedDateTimeColumn get somedate => _somedate ??= _constructSomedate();
GeneratedDateTimeColumn _constructSomedate() {
return GeneratedDateTimeColumn('somedate', $tableName, true,
$customConstraints: '');
}
@override
List<GeneratedColumn> get $columns => [someid, sometext];
List<GeneratedColumn> get $columns => [someid, sometext, somebool, somedate];
@override
Mytable get asDslTable => this;
@override
@ -770,6 +826,18 @@ class Mytable extends Table with TableInfo<Mytable, MytableData> {
} else if (sometext.isRequired && isInserting) {
context.missing(_sometextMeta);
}
if (d.somebool.present) {
context.handle(_someboolMeta,
somebool.isAcceptableValue(d.somebool.value, _someboolMeta));
} else if (somebool.isRequired && isInserting) {
context.missing(_someboolMeta);
}
if (d.somedate.present) {
context.handle(_somedateMeta,
somedate.isAcceptableValue(d.somedate.value, _somedateMeta));
} else if (somedate.isRequired && isInserting) {
context.missing(_somedateMeta);
}
return context;
}
@ -790,6 +858,12 @@ class Mytable extends Table with TableInfo<Mytable, MytableData> {
if (d.sometext.present) {
map['sometext'] = Variable<String, StringType>(d.sometext.value);
}
if (d.somebool.present) {
map['somebool'] = Variable<bool, BoolType>(d.somebool.value);
}
if (d.somedate.present) {
map['somedate'] = Variable<DateTime, DateTimeType>(d.somedate.value);
}
return map;
}

View File

@ -22,7 +22,9 @@ create table config (
CREATE TABLE mytable (
someid INTEGER NOT NULL PRIMARY KEY,
sometext TEXT
sometext TEXT,
somebool BOOLEAN,
somedate DATETIME
);
readConfig: SELECT * FROM config WHERE config_key = ?;

View File

@ -21,7 +21,9 @@ const _createConfig = 'CREATE TABLE IF NOT EXISTS config ('
const _createMyTable = 'CREATE TABLE IF NOT EXISTS mytable ('
'someid INTEGER NOT NULL PRIMARY KEY, '
'sometext VARCHAR);';
'sometext VARCHAR, '
'somebool INTEGER, '
'somedate INTEGER);';
void main() {
// see ../data/tables/tables.moor

View File

@ -37,7 +37,7 @@ void main() {
'CREATE TABLE IF NOT EXISTS users '
'(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, '
'name VARCHAR NOT NULL, '
'is_awesome BOOLEAN NOT NULL DEFAULT 1 CHECK (is_awesome in (0, 1)), '
'is_awesome INTEGER NOT NULL DEFAULT 1 CHECK (is_awesome in (0, 1)), '
'profile_picture BLOB NOT NULL, '
'creation_time INTEGER NOT NULL '
"DEFAULT (strftime('%s', CURRENT_TIMESTAMP)));",
@ -70,7 +70,7 @@ void main() {
'CREATE TABLE IF NOT EXISTS users '
'(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, '
'name VARCHAR NOT NULL, '
'is_awesome BOOLEAN NOT NULL DEFAULT 1 CHECK (is_awesome in (0, 1)), '
'is_awesome INTEGER NOT NULL DEFAULT 1 CHECK (is_awesome in (0, 1)), '
'profile_picture BLOB NOT NULL, '
'creation_time INTEGER NOT NULL '
"DEFAULT (strftime('%s', CURRENT_TIMESTAMP)));",
@ -88,7 +88,7 @@ void main() {
.addColumn(db.users, db.users.isAwesome);
verify(mockQueryExecutor.call('ALTER TABLE users ADD COLUMN '
'is_awesome BOOLEAN NOT NULL DEFAULT 1 CHECK (is_awesome in (0, 1));'));
'is_awesome INTEGER NOT NULL DEFAULT 1 CHECK (is_awesome in (0, 1));'));
});
});

View File

@ -17,7 +17,7 @@ class CreateTableReader {
CreateTableReader(this.stmt, this.step);
SpecifiedTable extractTable(TypeMapper mapper) {
final table = SchemaFromCreateTable().read(stmt);
final table = SchemaFromCreateTable(moorExtensions: true).read(stmt);
final foundColumns = <String, SpecifiedColumn>{};
final primaryKey = <SpecifiedColumn>{};

View File

@ -8,7 +8,9 @@ import 'relative_file.moor';
CREATE TABLE users(
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
name VARCHAR NOT NULL CHECK(LENGTH(name) BETWEEN 5 AND 30)
name VARCHAR NOT NULL CHECK(LENGTH(name) BETWEEN 5 AND 30),
field BOOLEAN,
another DATETIME
);
usersWithLongName: SELECT * FROM users WHERE LENGTH(name) > 25
@ -22,6 +24,9 @@ usersWithLongName: SELECT * FROM users WHERE LENGTH(name) > 25
final table = result.declaredTables.single;
expect(table.sqlName, 'users');
expect(table.columns.map((c) => c.name.name), ['id', 'name']);
expect(table.columns.map((c) => c.name.name),
['id', 'name', 'field', 'another']);
expect(table.columns.map((c) => c.dartTypeName),
['int', 'String', 'bool', 'DateTime']);
});
}

View File

@ -2,6 +2,12 @@ part of '../analysis.dart';
/// Reads the [Table] definition from a [CreateTableStatement].
class SchemaFromCreateTable {
/// Whether we should provide additional type hints for nonstandard `BOOL`
/// and `DATETIME` columns.
final bool moorExtensions;
SchemaFromCreateTable({this.moorExtensions = false});
Table read(CreateTableStatement stmt) {
return Table(
name: stmt.tableName,
@ -13,10 +19,12 @@ class SchemaFromCreateTable {
}
TableColumn _readColumn(ColumnDefinition definition) {
final affinity = columnAffinity(definition.typeName);
final typeName = definition.typeName.toUpperCase();
final type = resolveColumnType(typeName);
final nullable = !definition.constraints.any((c) => c is NotNull);
final resolvedType = ResolvedType(type: affinity, nullable: nullable);
final resolvedType = type.withNullable(nullable);
return TableColumn(
definition.columnName,
@ -25,29 +33,46 @@ class SchemaFromCreateTable {
);
}
/// Looks up the correct column affinity for a declared type name with the
/// rules described here:
/// Resolves a column type via its typename, see the linked rules below.
/// Additionally, if [moorExtensions] are enabled, we support [IsBoolean] and
/// [IsDateTime] hints if the type name contains `BOOL` or `DATE`,
/// respectively.
/// https://www.sqlite.org/datatype3.html#determination_of_column_affinity
@visibleForTesting
BasicType columnAffinity(String typeName) {
ResolvedType resolveColumnType(String typeName) {
if (typeName == null) {
return BasicType.blob;
return const ResolvedType(type: BasicType.blob);
}
final upper = typeName.toUpperCase();
if (upper.contains('INT')) {
return BasicType.int;
return const ResolvedType(type: BasicType.int);
}
if (upper.contains('CHAR') ||
upper.contains('CLOB') ||
upper.contains('TEXT')) {
return BasicType.text;
return const ResolvedType(type: BasicType.text);
}
if (upper.contains('BLOB')) {
return BasicType.blob;
return const ResolvedType(type: BasicType.blob);
}
return BasicType.real;
if (moorExtensions) {
if (upper.contains('BOOL')) {
return const ResolvedType.bool();
}
if (upper.contains('DATE')) {
return const ResolvedType(
type: BasicType.int, hint: const IsDateTime());
}
}
return const ResolvedType(type: BasicType.real);
}
/// Looks up the correct column affinity for a declared type name with the
/// rules described here:
/// https://www.sqlite.org/datatype3.html#determination_of_column_affinity
@visibleForTesting
BasicType columnAffinity(String typeName) => resolveColumnType(typeName).type;
}

View File

@ -63,6 +63,11 @@ class ResolvedType {
/// [ResolvedType.hint] to [IsBoolean].
abstract class TypeHint {
const TypeHint();
@override
int get hashCode => runtimeType.hashCode;
@override
bool operator ==(other) => other.runtimeType == runtimeType;
}
/// Type hint to mark that this type will contain a boolean value.

View File

@ -61,4 +61,22 @@ void main() {
expect(table.tableConstraints, hasLength(2));
});
test('supports booleans when moor extensions are enabled', () {
final engine = SqlEngine(useMoorExtensions: true);
final stmt = engine.parse('''
CREATE TABLE foo (
a BOOL, b DATETIME, c DATE, d BOOLEAN NOT NULL
)
''').rootNode;
final table = SchemaFromCreateTable(moorExtensions: true)
.read(stmt as CreateTableStatement);
expect(table.resolvedColumns.map((c) => c.type), const [
ResolvedType(type: BasicType.int, hint: IsBoolean(), nullable: true),
ResolvedType(type: BasicType.int, hint: IsDateTime(), nullable: true),
ResolvedType(type: BasicType.int, hint: IsDateTime(), nullable: true),
ResolvedType(type: BasicType.int, hint: IsBoolean(), nullable: false),
]);
});
}