Export table structure from CREATE TABLE statements

This commit is contained in:
Simon Binder 2019-07-28 22:09:20 +02:00
parent 3a2646e837
commit a550a49705
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
8 changed files with 164 additions and 21 deletions

View File

@ -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';

View File

@ -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<ColumnConstraint> 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

View File

@ -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;
}
}

View File

@ -30,8 +30,18 @@ class Table with ResultSet, VisibleToChildren {
@override
final List<TableColumn> resolvedColumns;
/// Whether this table was created with an "WITHOUT ROWID" modifier
final bool withoutRowId;
/// Additional constraints set on this table.
final List<TableConstraint> 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;
}

View File

@ -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>[];
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);
}

View File

@ -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));
});
}

View File

@ -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
)
''';

View File

@ -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,