diff --git a/sqlparser/lib/src/analysis/analysis.dart b/sqlparser/lib/src/analysis/analysis.dart index 59757772..d467477f 100644 --- a/sqlparser/lib/src/analysis/analysis.dart +++ b/sqlparser/lib/src/analysis/analysis.dart @@ -5,6 +5,7 @@ import 'package:sqlparser/sqlparser.dart'; import 'package:sqlparser/src/reader/tokenizer/token.dart'; part 'schema/column.dart'; +part 'schema/from_create_table.dart'; part 'schema/references.dart'; part 'schema/table.dart'; diff --git a/sqlparser/lib/src/analysis/schema/column.dart b/sqlparser/lib/src/analysis/schema/column.dart index e860ab6e..dc4c05ac 100644 --- a/sqlparser/lib/src/analysis/schema/column.dart +++ b/sqlparser/lib/src/analysis/schema/column.dart @@ -16,10 +16,16 @@ class TableColumn extends Column { /// The type of this column, which is immediately available. final ResolvedType type; + /// The column constraints set on this column. + /// + /// See also: + /// - https://www.sqlite.org/syntax/column-constraint.html + final List constraints; + /// The table this column belongs to. Table table; - TableColumn(this.name, this.type); + TableColumn(this.name, this.type, {this.constraints = const []}); } /// A column that is created by an expression. For instance, in the select diff --git a/sqlparser/lib/src/analysis/schema/from_create_table.dart b/sqlparser/lib/src/analysis/schema/from_create_table.dart new file mode 100644 index 00000000..47415d25 --- /dev/null +++ b/sqlparser/lib/src/analysis/schema/from_create_table.dart @@ -0,0 +1,52 @@ +part of '../analysis.dart'; + +/// Reads the [Table] definition from a [CreateTableStatement]. +class SchemaFromCreateTable { + Table read(CreateTableStatement stmt) { + return Table( + name: stmt.tableName, + resolvedColumns: [for (var def in stmt.columns) _readColumn(def)], + withoutRowId: stmt.withoutRowId, + tableConstraints: stmt.tableConstraints, + ); + } + + TableColumn _readColumn(ColumnDefinition definition) { + final affinity = columnAffinity(definition.typeName); + final nullable = !definition.constraints.any((c) => c is NotNull); + + final resolvedType = ResolvedType(type: affinity, nullable: nullable); + + return TableColumn( + definition.columnName, + resolvedType, + constraints: definition.constraints, + ); + } + + /// 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) { + if (typeName == null) { + return BasicType.blob; + } + + final upper = typeName.toUpperCase(); + if (upper.contains('INT')) { + return BasicType.int; + } + if (upper.contains('CHAR') || + upper.contains('CLOB') || + upper.contains('TEXT')) { + return BasicType.text; + } + + if (upper.contains('BLOB')) { + return BasicType.blob; + } + + return BasicType.real; + } +} diff --git a/sqlparser/lib/src/analysis/schema/table.dart b/sqlparser/lib/src/analysis/schema/table.dart index ee53adb9..ded3945c 100644 --- a/sqlparser/lib/src/analysis/schema/table.dart +++ b/sqlparser/lib/src/analysis/schema/table.dart @@ -30,8 +30,18 @@ class Table with ResultSet, VisibleToChildren { @override final List resolvedColumns; + /// Whether this table was created with an "WITHOUT ROWID" modifier + final bool withoutRowId; + + /// Additional constraints set on this table. + final List tableConstraints; + /// Constructs a table from the known [name] and [resolvedColumns]. - Table({@required this.name, this.resolvedColumns}) { + Table( + {@required this.name, + this.resolvedColumns, + this.withoutRowId = false, + this.tableConstraints = const []}) { for (var column in resolvedColumns) { column.table = this; } diff --git a/sqlparser/lib/src/reader/parser/schema.dart b/sqlparser/lib/src/reader/parser/schema.dart index d3f49921..78ac76e7 100644 --- a/sqlparser/lib/src/reader/parser/schema.dart +++ b/sqlparser/lib/src/reader/parser/schema.dart @@ -1,5 +1,12 @@ part of 'parser.dart'; +const _tokensInTypename = [ + TokenType.identifier, + TokenType.leftParen, + TokenType.rightParen, + TokenType.numberLiteral, +]; + mixin SchemaParser on ParserBase { CreateTableStatement _createTable() { if (!_matchOne(TokenType.create)) return null; @@ -62,12 +69,15 @@ mixin SchemaParser on ParserBase { ColumnDefinition _columnDefinition() { final name = _consume(TokenType.identifier, 'Expected a column name') as IdentifierToken; - IdentifierToken typeName; - if (_matchOne(TokenType.identifier)) { - typeName = _previous as IdentifierToken; + final typeNameBuilder = StringBuffer(); + while (_match(_tokensInTypename)) { + typeNameBuilder.write(_previous.lexeme); } + final typeName = + typeNameBuilder.isEmpty ? null : typeNameBuilder.toString(); + final constraints = []; ColumnConstraint constraint; while ((constraint = _columnConstraint(orNull: true)) != null) { @@ -76,7 +86,7 @@ mixin SchemaParser on ParserBase { return ColumnDefinition( columnName: name.identifier, - typeName: typeName?.identifier, + typeName: typeName, constraints: constraints, )..setSpan(name, _previous); } diff --git a/sqlparser/test/analysis/schema/from_create_table.dart b/sqlparser/test/analysis/schema/from_create_table.dart new file mode 100644 index 00000000..1bccd666 --- /dev/null +++ b/sqlparser/test/analysis/schema/from_create_table.dart @@ -0,0 +1,64 @@ +import 'package:sqlparser/sqlparser.dart'; +import 'package:test/test.dart'; + +import '../../common_data.dart'; + +const _affinityTests = { + 'INT': BasicType.int, + 'INTEGER': BasicType.int, + 'TINYINT': BasicType.int, + 'SMALLINT': BasicType.int, + 'MEDIUMINT': BasicType.int, + 'BIGINT': BasicType.int, + 'UNISGNED BIG INT': BasicType.int, + 'INT2': BasicType.int, + 'INT8': BasicType.int, + 'CHARACTER(20)': BasicType.text, + 'CHARACTER(255)': BasicType.text, + 'VARYING CHARACTER(255)': BasicType.text, + 'NCHAR(55)': BasicType.text, + 'NATIVE CHARACTER(70)': BasicType.text, + 'NVARCHAR(100)': BasicType.text, + 'TEXT': BasicType.text, + 'CLOB': BasicType.text, + 'BLOB': BasicType.blob, + null: BasicType.blob, + 'REAL': BasicType.real, + 'DOUBLE': BasicType.real, + 'DOUBLE PRECISION': BasicType.real, + 'FLOAT': BasicType.real, + 'NUMERIC': BasicType.real, + 'DECIMAL(10,5)': BasicType.real, + 'BOOLEAN': BasicType.real, + 'DATE': BasicType.real, + 'DATETIME': BasicType.real, +}; + +void main() { + test('affinity from typename', () { + final resolver = SchemaFromCreateTable(); + + _affinityTests.forEach((key, value) { + expect(resolver.columnAffinity(key), equals(value), + reason: '$key should have $value affinity'); + }); + }); + + test('export table structure', () { + final engine = SqlEngine(); + final stmt = engine.parse(createTableStmt).rootNode; + + final table = SchemaFromCreateTable().read(stmt as CreateTableStatement); + + expect(table.resolvedColumns.map((c) => c.name), + ['id', 'email', 'score', 'display_name']); + expect(table.resolvedColumns.map((c) => c.type), const [ + ResolvedType(type: BasicType.int), + ResolvedType(type: BasicType.text), + ResolvedType(type: BasicType.int), + ResolvedType(type: BasicType.text, nullable: true), + ]); + + expect(table.tableConstraints, hasLength(2)); + }); +} diff --git a/sqlparser/test/common_data.dart b/sqlparser/test/common_data.dart new file mode 100644 index 00000000..5a47765d --- /dev/null +++ b/sqlparser/test/common_data.dart @@ -0,0 +1,13 @@ +const createTableStmt = ''' +CREATE TABLE IF NOT EXISTS users ( + id INT NOT NULL PRIMARY KEY DESC ON CONFLICT ROLLBACK AUTOINCREMENT, + email VARCHAR NOT NULL UNIQUE ON CONFLICT ABORT, + score INT CONSTRAINT "score set" NOT NULL DEFAULT 420 CHECK (score > 0), + display_name VARCHAR COLLATE BINARY + REFERENCES some(thing) ON UPDATE CASCADE ON DELETE SET NULL, + + UNIQUE (score, display_name) ON CONFLICT ABORT, + FOREIGN KEY (id, email) REFERENCES another (a, b) + ON DELETE NO ACTION ON UPDATE RESTRICT +) +'''; diff --git a/sqlparser/test/parser/create_table_test.dart b/sqlparser/test/parser/create_table_test.dart index 695274df..57cf5295 100644 --- a/sqlparser/test/parser/create_table_test.dart +++ b/sqlparser/test/parser/create_table_test.dart @@ -1,24 +1,11 @@ import 'package:sqlparser/src/ast/ast.dart'; +import '../common_data.dart'; import 'utils.dart'; -final statement = ''' -CREATE TABLE IF NOT EXISTS users ( - id INT NOT NULL PRIMARY KEY DESC ON CONFLICT ROLLBACK AUTOINCREMENT, - email VARCHAR NOT NULL UNIQUE ON CONFLICT ABORT, - score INT CONSTRAINT "score set" NOT NULL DEFAULT 420 CHECK (score > 0), - display_name VARCHAR COLLATE BINARY - REFERENCES some(thing) ON UPDATE CASCADE ON DELETE SET NULL, - - UNIQUE (score, display_name) ON CONFLICT ABORT, - FOREIGN KEY (id, email) REFERENCES another (a, b) - ON DELETE NO ACTION ON UPDATE RESTRICT -) -'''; - void main() { testStatement( - statement, + createTableStmt, CreateTableStatement( tableName: 'users', ifNotExists: true,