From 5909b0d3a25e2bc78552b512e4b8f0f705dd48de Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 13 Feb 2019 20:04:13 +0100 Subject: [PATCH] Enforce text length, escape table and column names --- sally/README.md | 1 - sally/lib/src/runtime/structure/columns.dart | 24 ++- .../lib/src/model/specified_column.dart | 29 ++++ .../lib/src/parser/column_parser.dart | 4 +- .../lib/src/parser/table_parser.dart | 3 +- sally_generator/lib/src/sqlite_keywords.dart | 147 ++++++++++++++++++ 6 files changed, 203 insertions(+), 5 deletions(-) create mode 100644 sally_generator/lib/src/sqlite_keywords.dart diff --git a/sally/README.md b/sally/README.md index f244e1b1..1b1e166b 100644 --- a/sally/README.md +++ b/sally/README.md @@ -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 diff --git a/sally/lib/src/runtime/structure/columns.dart b/sally/lib/src/runtime/structure/columns.dart index ea165e09..e57640e9 100644 --- a/sally/lib/src/runtime/structure/columns.dart +++ b/sally/lib/src/runtime/structure/columns.dart @@ -25,6 +25,7 @@ abstract class GeneratedColumn> extends Column { @visibleForOverriding void writeCustomConstraints(StringBuffer into) {} + @visibleForOverriding String get typeName; @@ -54,7 +55,12 @@ abstract class GeneratedColumn> extends Column { class GeneratedTextColumn extends GeneratedColumn 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 like(String regex) => @@ -62,6 +68,22 @@ class GeneratedTextColumn extends GeneratedColumn @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 diff --git a/sally_generator/lib/src/model/specified_column.dart b/sally_generator/lib/src/model/specified_column.dart index 22c325c2..f7b326a7 100644 --- a/sally_generator/lib/src/model/specified_column.dart +++ b/sally_generator/lib/src/model/specified_column.dart @@ -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 { /// 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._(); + 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 { ..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 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', diff --git a/sally_generator/lib/src/parser/column_parser.dart b/sally_generator/lib/src/parser/column_parser.dart index 02fb509f..339b94a1 100644 --- a/sally_generator/lib/src/parser/column_parser.dart +++ b/sally_generator/lib/src/parser/column_parser.dart @@ -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); } diff --git a/sally_generator/lib/src/parser/table_parser.dart b/sally_generator/lib/src/parser/table_parser.dart index cfe7938e..09d57923 100644 --- a/sally_generator/lib/src/parser/table_parser.dart +++ b/sally_generator/lib/src/parser/table_parser.dart @@ -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)); } diff --git a/sally_generator/lib/src/sqlite_keywords.dart b/sally_generator/lib/src/sqlite_keywords.dart new file mode 100644 index 00000000..0015dde8 --- /dev/null +++ b/sally_generator/lib/src/sqlite_keywords.dart @@ -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; +} \ No newline at end of file