Enforce text length, escape table and column names

This commit is contained in:
Simon Binder 2019-02-13 20:04:13 +01:00
parent c9aab2e824
commit 5909b0d3a2
6 changed files with 203 additions and 5 deletions

View File

@ -103,7 +103,6 @@ create an issue.
- Custom primary keys
- Stabilize all end-user APIs and document them extensively
- Support default values and expressions, auto-increment
- Escape table and column names
- Auto-updating streams for select statements
##### Definitely planned for the future
- Allow using DAOs instead of having to put everything in the main database

View File

@ -25,6 +25,7 @@ abstract class GeneratedColumn<T, S extends SqlType<T>> extends Column<T, S> {
@visibleForOverriding
void writeCustomConstraints(StringBuffer into) {}
@visibleForOverriding
String get typeName;
@ -54,7 +55,12 @@ abstract class GeneratedColumn<T, S extends SqlType<T>> extends Column<T, S> {
class GeneratedTextColumn extends GeneratedColumn<String, StringType>
implements TextColumn {
GeneratedTextColumn(String name, bool nullable) : super(name, nullable);
final int minTextLength;
final int maxTextLength;
GeneratedTextColumn(String name, bool nullable,
{this.minTextLength, this.maxTextLength})
: super(name, nullable);
@override
Expression<BoolType> like(String regex) =>
@ -62,6 +68,22 @@ class GeneratedTextColumn extends GeneratedColumn<String, StringType>
@override
final String typeName = 'VARCHAR';
@override
bool isAcceptableValue(String value, bool duringInsert) {
final nullOk = !duringInsert || $nullable;
if (value == null) return nullOk;
final length = value.length;
if (minTextLength != null && minTextLength > length)
return false;
if (maxTextLength != null && maxTextLength < length)
return false;
return true;
}
}
class GeneratedBoolColumn extends GeneratedColumn<bool, BoolType>

View File

@ -1,9 +1,13 @@
import 'package:built_value/built_value.dart';
import 'package:sally_generator/src/sqlite_keywords.dart' show isSqliteKeyword;
part 'specified_column.g.dart';
enum ColumnType { integer, text, boolean }
/// Name of a column. Contains additional info on whether the name was chosen
/// implicitly (based on the dart getter name) or explicitly (via an named())
/// call in the column builder dsl.
abstract class ColumnName implements Built<ColumnName, ColumnNameBuilder> {
/// A column name is implicit if it has been looked up with the associated
/// field name in the table class. It's explicit if `.named()` was called in
@ -14,6 +18,14 @@ abstract class ColumnName implements Built<ColumnName, ColumnNameBuilder> {
ColumnName._();
ColumnName escapeIfSqlKeyword() {
if (isSqliteKeyword(name)) {
return rebuild((b) => b.name = '`$name`'); // wrap name in backticks
} else {
return this;
}
}
factory ColumnName([updates(ColumnNameBuilder b)]) = _$ColumnName;
factory ColumnName.implicitly(String name) => ColumnName((b) => b
@ -25,11 +37,18 @@ abstract class ColumnName implements Built<ColumnName, ColumnNameBuilder> {
..name = name);
}
/// A column, as specified by a getter in a table.
class SpecifiedColumn {
/// The getter name of this column in the table class. It will also be used
/// as getter name in the TableInfo class (as it needs to override the field)
/// and in the generated data class that will be generated for each table.
final String dartGetterName;
/// The sql type of this column
final ColumnType type;
/// The name of this column, as chosen by the user
final ColumnName name;
/// Whether this column has auto increment.
bool get hasAI => features.any((f) => f is AutoIncrement);
/// Whether this column has been declared as the primary key via the
@ -38,24 +57,34 @@ class SpecifiedColumn {
final bool declaredAsPrimaryKey;
final List<ColumnFeature> features;
/// The dart type that matches this column. For instance, if a table has
/// declared an `IntColumn`, the matching dart type name would be [int].
String get dartTypeName => {
ColumnType.boolean: 'bool',
ColumnType.text: 'String',
ColumnType.integer: 'int'
}[type];
/// The column type from the dsl library. For instance, if a table has
/// declared an `IntColumn`, the matching dsl column name would also be an
/// `IntColumn`.
String get dslColumnTypeName => {
ColumnType.boolean: 'BoolColumn',
ColumnType.text: 'TextColumn',
ColumnType.integer: 'IntColumn'
}[type];
/// The `GeneratedColumn` class that implements the [dslColumnTypeName].
/// For instance, if a table has declared an `IntColumn`, the matching
/// implementation name would be an `GeneratedIntColumn`.
String get implColumnTypeName => {
ColumnType.boolean: 'GeneratedBoolColumn',
ColumnType.text: 'GeneratedTextColumn',
ColumnType.integer: 'GeneratedIntColumn'
}[type];
/// The class inside the sally library that represents the same sql type as
/// this column.
String get sqlTypeName => {
ColumnType.boolean: 'BoolType',
ColumnType.text: 'StringType',

View File

@ -89,7 +89,7 @@ class ColumnParser extends ParserBase {
wasDeclaredAsPrimaryKey = true;
break;
case functionReferences:
break; // todo: parsing this is going to suck
break;
case functionWithLength:
final args = remainingExpr.argumentList;
final minArg = findNamedArgument(args, 'min');
@ -121,7 +121,7 @@ class ColumnParser extends ParserBase {
return SpecifiedColumn(
type: _startMethodToColumnType(foundStartMethod),
dartGetterName: getter.name.name,
name: name,
name: name.escapeIfSqlKeyword(),
declaredAsPrimaryKey: wasDeclaredAsPrimaryKey,
features: foundFeatures);
}

View File

@ -4,6 +4,7 @@ import 'package:sally_generator/src/errors.dart';
import 'package:sally_generator/src/model/specified_column.dart';
import 'package:sally_generator/src/model/specified_table.dart';
import 'package:sally_generator/src/parser/parser.dart';
import 'package:sally_generator/src/sqlite_keywords.dart';
import 'package:sally_generator/src/utils/names.dart';
import 'package:sally_generator/src/utils/type_utils.dart';
import 'package:sally_generator/src/sally_generator.dart'; // ignore: implementation_imports
@ -18,7 +19,7 @@ class TableParser extends ParserBase {
return SpecifiedTable(
fromClass: element,
columns: _parseColumns(element),
sqlName: sqlName,
sqlName: escapeIfNeeded(sqlName),
dartTypeName: _readDartTypeName(element));
}

View File

@ -0,0 +1,147 @@
// https://www.sqlite.org/lang_keywords.html
const sqliteKeywords = [
'ABORT',
'ACTION',
'ADD',
'AFTER',
'ALL',
'ALTER',
'ANALYZE',
'AND',
'AS',
'ASC',
'ATTACH',
'AUTOINCREMENT',
'BEFORE',
'BEGIN',
'BETWEEN',
'BY',
'CASCADE',
'CASE',
'CAST',
'CHECK',
'COLLATE',
'COLUMN',
'COMMIT',
'CONFLICT',
'CONSTRAINT',
'CREATE',
'CROSS',
'CURRENT',
'CURRENT_DATE',
'CURRENT_TIME',
'CURRENT_TIMESTAMP',
'DATABASE',
'DEFAULT',
'DEFERRABLE',
'DEFERRED',
'DELETE',
'DESC',
'DETACH',
'DISTINCT',
'DO',
'DROP',
'EACH',
'ELSE',
'END',
'ESCAPE',
'EXCEPT',
'EXCLUSIVE',
'EXISTS',
'EXPLAIN',
'FAIL',
'FILTER',
'FOLLOWING',
'FOR',
'FOREIGN',
'FROM',
'FULL',
'GLOB',
'GROUP',
'HAVING',
'IF',
'IGNORE',
'IMMEDIATE',
'IN',
'INDEX',
'INDEXED',
'INITIALLY',
'INNER',
'INSERT',
'INSTEAD',
'INTERSECT',
'INTO',
'IS',
'ISNULL',
'JOIN',
'KEY',
'LEFT',
'LIKE',
'LIMIT',
'MATCH',
'NATURAL',
'NO',
'NOT',
'NOTHING',
'NOTNULL',
'NULL',
'OF',
'OFFSET',
'ON',
'OR',
'ORDER',
'OUTER',
'OVER',
'PARTITION',
'PLAN',
'PRAGMA',
'PRECEDING',
'PRIMARY',
'QUERY',
'RAISE',
'RANGE',
'RECURSIVE',
'REFERENCES',
'REGEXP',
'REINDEX',
'RELEASE',
'RENAME',
'REPLACE',
'RESTRICT',
'RIGHT',
'ROLLBACK',
'ROW',
'ROWS',
'SAVEPOINT',
'SELECT',
'SET',
'TABLE',
'TEMP',
'TEMPORARY',
'THEN',
'TO',
'TRANSACTION',
'TRIGGER',
'UNBOUNDED',
'UNION',
'UNIQUE',
'UPDATE',
'USING',
'VACUUM',
'VALUES',
'VIEW',
'VIRTUAL',
'WHEN',
'WHERE',
'WINDOW',
'WITH',
'WITHOUT'
];
bool isSqliteKeyword(String s) => sqliteKeywords.contains(s.toUpperCase());
String escapeIfNeeded(String s) {
if (isSqliteKeyword(s))
return '`$s`';
return s;
}