mirror of https://github.com/AMT-Cheif/drift.git
Export table structure from CREATE TABLE statements
This commit is contained in:
parent
3a2646e837
commit
a550a49705
|
@ -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';
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
}
|
|
@ -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
|
||||
)
|
||||
''';
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue