/// Library to convert AST nodes back to text. /// /// See the [NodeToText] extension for details. library utils.node_to_text; import 'package:charcode/charcode.dart'; import 'package:sqlparser/sqlparser.dart'; class NodeSqlBuilder extends AstVisitor { final StringSink buffer; /// Whether we need to insert a space before writing the next identifier. bool needsSpace = false; NodeSqlBuilder([StringSink? buffer]) : buffer = buffer ?? StringBuffer(); /// Writes a space character if [needsSpace] is set. /// /// This also resets [needsSpace] to `false`. void spaceIfNeeded() { if (needsSpace) { needsSpace = false; _space(); } } bool isKeyword(String lexeme) { return isKeywordLexeme(lexeme); } String escapeIdentifier(String identifier) { return '"$identifier"'; } @override void visitAggregateFunctionInvocation( AggregateFunctionInvocation e, void arg) { symbol(e.name, spaceBefore: true); symbol('('); visit(e.parameters, arg); visitNullable(e.orderBy, arg); symbol(')'); if (e.filter != null) { keyword(TokenType.filter); symbol('(', spaceBefore: true); keyword(TokenType.where); visit(e.filter!, arg); symbol(')', spaceAfter: true); } } @override void visitWindowFunctionInvocation(WindowFunctionInvocation e, void arg) { visitAggregateFunctionInvocation(e, arg); if (e.windowDefinition != null) { keyword(TokenType.over); visit(e.windowDefinition!, arg); } else if (e.windowName != null) { keyword(TokenType.over); identifier(e.windowName!); } } @override void visitBeginTransaction(BeginTransactionStatement e, void arg) { keyword(TokenType.begin); switch (e.mode) { case TransactionMode.none: break; case TransactionMode.deferred: keyword(TokenType.deferred); break; case TransactionMode.immediate: keyword(TokenType.immediate); break; case TransactionMode.exclusive: keyword(TokenType.exclusive); break; } } @override void visitBetweenExpression(BetweenExpression e, void arg) { visit(e.check, arg); if (e.not) { keyword(TokenType.not); } keyword(TokenType.between); visit(e.lower, arg); keyword(TokenType.and); visit(e.upper, arg); } @override void visitBinaryExpression(BinaryExpression e, void arg) { visit(e.left, arg); final operatorSymbol = const { TokenType.doublePipe: '||', TokenType.star: '*', TokenType.slash: '/', TokenType.percent: '%', TokenType.plus: '+', TokenType.minus: '-', TokenType.shiftLeft: '<<', TokenType.shiftRight: '>>', TokenType.ampersand: '&', TokenType.pipe: '|', TokenType.less: '<', TokenType.lessEqual: '<=', TokenType.more: '>', TokenType.moreEqual: '>=', TokenType.equal: '=', TokenType.doubleEqual: '==', TokenType.exclamationEqual: '!=', TokenType.lessMore: '<>', TokenType.dashRangle: '->', TokenType.dashRangleRangle: '->>', }[e.operator.type]; if (operatorSymbol != null) { symbol(operatorSymbol, spaceBefore: true, spaceAfter: true); } else { keyword(e.operator.type); } visit(e.right, arg); } void _writeStatements(Iterable statements) { for (final stmt in statements) { visit(stmt, null); symbol(';'); } } @override void visitBlock(Block block, void arg) { keyword(TokenType.begin); _writeStatements(block.statements); keyword(TokenType.end); } @override void visitBooleanLiteral(BooleanLiteral e, void arg) { keyword(e.value ? TokenType.$true : TokenType.$false); } @override void visitCaseExpression(CaseExpression e, void arg) { keyword(TokenType.$case); visitNullable(e.base, arg); visitList(e.whens, arg); final elseExpr = e.elseExpr; if (elseExpr != null) { keyword(TokenType.$else); visit(elseExpr, arg); } keyword(TokenType.end); } @override void visitCastExpression(CastExpression e, void arg) { keyword(TokenType.cast); symbol('('); visit(e.operand, arg); keyword(TokenType.as); symbol(e.typeName, spaceBefore: true); symbol(')', spaceAfter: true); } @override void visitCollateExpression(CollateExpression e, void arg) { visit(e.inner, arg); keyword(TokenType.collate); identifier(e.collation); } @override void visitColumnConstraint(ColumnConstraint e, void arg) { if (e.name != null) { keyword(TokenType.constraint); identifier(e.name!); } e.when( primaryKey: (primaryKey) { keyword(TokenType.primary); keyword(TokenType.key); _orderingMode(primaryKey.mode); _conflictClause(primaryKey.onConflict); if (primaryKey.autoIncrement) keyword(TokenType.autoincrement); }, notNull: (notNull) { keyword(TokenType.not); keyword(TokenType.$null); _conflictClause(notNull.onConflict); }, nullable: (nullable) { keyword(TokenType.$null); }, unique: (unique) { keyword(TokenType.unique); _conflictClause(unique.onConflict); }, check: (check) { keyword(TokenType.check); symbol('(', spaceBefore: true); visit(check.expression, arg); symbol(')', spaceAfter: true); }, isDefault: (def) { keyword(TokenType.$default); final expr = def.expression; if (expr is Literal) { visit(expr, arg); } else { symbol('(', spaceBefore: true); visit(expr, arg); symbol(')', spaceAfter: true); } }, collate: (collate) { keyword(TokenType.collate); identifier(collate.collation); }, foreignKey: (foreignKey) { visit(foreignKey.clause, arg); }, generatedAs: (generatedAs) { keyword(TokenType.generated); keyword(TokenType.always); keyword(TokenType.as); symbol('(', spaceBefore: true); visit(generatedAs.expression, arg); symbol(')', spaceAfter: true); if (generatedAs.stored) { keyword(TokenType.stored); } else { keyword(TokenType.virtual); } }, mappedBy: (mappedBy) { keyword(TokenType.mapped); keyword(TokenType.by); dartCode(mappedBy.mapper.dartCode); }, ); } @override void visitColumnDefinition(ColumnDefinition e, void arg) { identifier(e.columnName); if (e.typeName != null) { symbol(e.typeName!, spaceAfter: true, spaceBefore: true); } visitList(e.constraints, arg); } @override void visitCommitStatement(CommitStatement e, void arg) { keyword(TokenType.commit); } @override void visitCommonTableExpression(CommonTableExpression e, void arg) { identifier(e.cteTableName); if (e.columnNames != null) { symbol('(', spaceBefore: true); var first = true; for (final columnName in e.columnNames!) { if (!first) { symbol(',', spaceAfter: true); } identifier(columnName, spaceBefore: !first, spaceAfter: false); first = false; } symbol(')', spaceAfter: true); } keyword(TokenType.as); switch (e.materializationHint) { case MaterializationHint.notMaterialized: keyword(TokenType.not); keyword(TokenType.materialized); break; case MaterializationHint.materialized: keyword(TokenType.materialized); break; case null: break; } symbol('(', spaceBefore: true); visit(e.as, arg); symbol(')', spaceAfter: true); } @override void visitCompoundSelectPart(CompoundSelectPart e, void arg) { switch (e.mode) { case CompoundSelectMode.union: keyword(TokenType.union); break; case CompoundSelectMode.unionAll: keyword(TokenType.union); keyword(TokenType.all); break; case CompoundSelectMode.intersect: keyword(TokenType.intersect); break; case CompoundSelectMode.except: keyword(TokenType.except); break; } visit(e.select, arg); } @override void visitCompoundSelectStatement(CompoundSelectStatement e, void arg) { visitNullable(e.withClause, arg); visit(e.base, arg); visitList(e.additional, arg); } @override void visitCreateIndexStatement(CreateIndexStatement e, void arg) { keyword(TokenType.create); if (e.unique) { keyword(TokenType.unique); } keyword(TokenType.$index); _ifNotExists(e.ifNotExists); identifier(e.indexName); keyword(TokenType.on); visit(e.on, arg); symbol('(', spaceBefore: true); _join(e.columns, ','); symbol(')', spaceAfter: true); _where(e.where); } @override void visitCreateTableStatement(CreateTableStatement e, void arg) { keyword(TokenType.create); keyword(TokenType.table); _ifNotExists(e.ifNotExists); identifier(e.tableName); symbol('('); _join([...e.columns, ...e.tableConstraints], ','); symbol(')'); if (e.withoutRowId) { keyword(TokenType.without); keyword(TokenType.rowid); } if (e.isStrict) { if (e.withoutRowId) symbol(','); keyword(TokenType.strict); } e.driftTableName?.accept(this, arg); } @override void visitCreateTriggerStatement(CreateTriggerStatement e, void arg) { keyword(TokenType.create); keyword(TokenType.trigger); _ifNotExists(e.ifNotExists); identifier(e.triggerName); switch (e.mode) { case TriggerMode.before: keyword(TokenType.before); break; case TriggerMode.after: keyword(TokenType.after); break; case TriggerMode.insteadOf: keyword(TokenType.instead); keyword(TokenType.of); break; default: // Can happen if e.mode == null break; } visit(e.target, arg); keyword(TokenType.on); visit(e.onTable, arg); if (e.when != null) { keyword(TokenType.when); visit(e.when!, arg); } visit(e.action, arg); } @override void visitCreateViewStatement(CreateViewStatement e, void arg) { keyword(TokenType.create); keyword(TokenType.view); _ifNotExists(e.ifNotExists); identifier(e.viewName); e.driftTableName?.accept(this, arg); if (e.columns != null) { symbol('(', spaceBefore: true); symbol(e.columns!.join(',')); symbol(')', spaceAfter: true); } keyword(TokenType.as); visit(e.query, arg); } @override void visitCreateVirtualTableStatement( CreateVirtualTableStatement e, void arg) { keyword(TokenType.create); keyword(TokenType.virtual); keyword(TokenType.table); _ifNotExists(e.ifNotExists); identifier(e.tableName); keyword(TokenType.using); identifier(e.moduleName); symbol('(${e.argumentContent.join(', ')})'); } @override void visitDriftSpecificNode(DriftSpecificNode e, void arg) { if (e is DartPlaceholder) { e.when( isLimit: (_) => keyword(TokenType.limit), isOrderBy: (_) { keyword(TokenType.order); keyword(TokenType.by); }, ); symbol(r'$', spaceBefore: true); symbol(e.name, spaceAfter: true); } else if (e is DeclaredStatement) { identifier(e.identifier.name); if (e.parameters.isNotEmpty) { symbol('('); _join(e.parameters, ','); symbol(')'); } visitNullable(e.as, arg); symbol(':', spaceAfter: true); visit(e.statement, arg); symbol(';'); } else if (e is DriftFile) { for (final stmt in e.statements) { visit(stmt, arg); buffer.write('\n'); needsSpace = false; } } else if (e is ImportStatement) { keyword(TokenType.import); _stringLiteral(e.importedFile); symbol(';', spaceAfter: true); } else if (e is StatementParameter) { if (e is VariableTypeHint) { if (e.isRequired) keyword(TokenType.required); visit(e.variable, arg); final typeName = e.typeName; if (typeName != null) { keyword(TokenType.as); symbol(typeName, spaceBefore: true, spaceAfter: true); } if (e.orNull) { keyword(TokenType.or); keyword(TokenType.$null); } } else if (e is DartPlaceholderDefaultValue) { symbol('\$${e.variableName}', spaceAfter: true); symbol('=', spaceBefore: true, spaceAfter: true); visit(e.defaultValue, arg); } else { throw AssertionError('Unknown StatementParameter: $e'); } } else if (e is DriftTableName) { keyword(e.useExistingDartClass ? TokenType.$with : TokenType.as); identifier(e.overriddenDataClassName); final constructor = e.constructorName; if (constructor != null) { symbol('.'); identifier(constructor); } } else if (e is NestedStarResultColumn) { identifier(e.tableName); symbol('.**', spaceAfter: true); } else if (e is NestedQueryColumn) { symbol('LIST(', spaceBefore: true); visit(e.select, arg); symbol(')', spaceAfter: true); if (e.as != null) { keyword(TokenType.as); identifier(e.as!); } } else if (e is TransactionBlock) { visit(e.begin, arg); _writeStatements(e.innerStatements); visit(e.commit, arg); } } @override void visitDefaultValues(DefaultValues e, void arg) { keyword(TokenType.$default); keyword(TokenType.$values); } @override void visitDeferrableClause(DeferrableClause e, void arg) { if (e.not) { keyword(TokenType.not); } keyword(TokenType.deferrable); switch (e.declaredInitially) { case InitialDeferrableMode.deferred: keyword(TokenType.initially); keyword(TokenType.deferred); break; case InitialDeferrableMode.immediate: keyword(TokenType.initially); keyword(TokenType.immediate); break; default: // declaredInitially == null, don't do anything break; } } @override void visitDeleteStatement(DeleteStatement e, void arg) { visitNullable(e.withClause, arg); keyword(TokenType.delete); _from(e.from); _where(e.where); visitNullable(e.returning, arg); } @override void visitDeleteTriggerTarget(DeleteTarget e, void arg) { keyword(TokenType.delete); } @override void visitDoNothing(DoNothing e, void arg) { keyword(TokenType.nothing); } @override void visitDoUpdate(DoUpdate e, void arg) { keyword(TokenType.update); keyword(TokenType.set); _join(e.set, ','); _where(e.where); } @override void visitExists(ExistsExpression e, void arg) { keyword(TokenType.exists); symbol('(', spaceBefore: true); visit(e.select, null); symbol(')', spaceAfter: true); } @override void visitExpressionFunctionParameters(ExprFunctionParameters e, void arg) { if (e.distinct) { keyword(TokenType.distinct); } _join(e.parameters, ','); } @override void visitExpressionResultColumn(ExpressionResultColumn e, void arg) { visit(e.expression, arg); visitNullable(e.mappedBy, arg); if (e.as != null) { keyword(TokenType.as); identifier(e.as!); } } @override void visitForeignKeyClause(ForeignKeyClause e, void arg) { keyword(TokenType.references); visit(e.foreignTable, arg); if (e.columnNames.isNotEmpty) { symbol('('); _join(e.columnNames, ','); symbol(')'); } void referenceAction(ReferenceAction action) { switch (action) { case ReferenceAction.setNull: keyword(TokenType.set); keyword(TokenType.$null); break; case ReferenceAction.setDefault: keyword(TokenType.set); keyword(TokenType.$default); break; case ReferenceAction.cascade: keyword(TokenType.cascade); break; case ReferenceAction.restrict: keyword(TokenType.restrict); break; case ReferenceAction.noAction: keyword(TokenType.no); keyword(TokenType.action); break; } } if (e.onUpdate != null) { keyword(TokenType.on); keyword(TokenType.update); referenceAction(e.onUpdate!); } if (e.onDelete != null) { keyword(TokenType.on); keyword(TokenType.delete); referenceAction(e.onDelete!); } visitNullable(e.deferrable, arg); } @override void visitFrameSpec(FrameSpec e, void arg) { void frameBoundary(FrameBoundary boundary) { void precedingOrFollowing(bool preceding) { if (boundary.isUnbounded) { keyword(TokenType.unbounded); } else { visit(boundary.offset!, arg); } keyword(preceding ? TokenType.preceding : TokenType.following); } if (boundary.isCurrentRow) { keyword(TokenType.current); keyword(TokenType.row); } else if (boundary.preceding) { precedingOrFollowing(true); } else { precedingOrFollowing(false); } } keyword(const { FrameType.range: TokenType.range, FrameType.rows: TokenType.rows, FrameType.groups: TokenType.groups, }[e.type!]!); keyword(TokenType.between); frameBoundary(e.start); keyword(TokenType.and); frameBoundary(e.end); if (e.excludeMode != null) { keyword(TokenType.exclude); switch (e.excludeMode!) { case ExcludeMode.noOthers: keyword(TokenType.no); keyword(TokenType.others); break; case ExcludeMode.currentRow: keyword(TokenType.current); keyword(TokenType.row); break; case ExcludeMode.group: keyword(TokenType.group); break; case ExcludeMode.ties: keyword(TokenType.ties); break; } } } @override void visitFunction(FunctionExpression e, void arg) { identifier(e.name); symbol('('); visit(e.parameters, arg); symbol(')', spaceAfter: true); } @override void visitGroupBy(GroupBy e, void arg) { keyword(TokenType.group); keyword(TokenType.by); _join(e.by, ','); if (e.having != null) { keyword(TokenType.having); visit(e.having!, arg); } } @override void visitIndexedColumn(IndexedColumn e, void arg) { visit(e.expression, arg); _orderingMode(e.ordering); } @override void visitInExpression(InExpression e, void arg) { visit(e.left, arg); if (e.not) { keyword(TokenType.not); } keyword(TokenType.$in); visit(e.inside, arg); } @override void visitInsertStatement(InsertStatement e, void arg) { visitNullable(e.withClause, arg); final mode = e.mode; if (mode == InsertMode.insert) { keyword(TokenType.insert); } else if (mode == InsertMode.replace) { keyword(TokenType.replace); } else { keyword(TokenType.insert); keyword(TokenType.or); keyword(const { InsertMode.insertOrReplace: TokenType.replace, InsertMode.insertOrRollback: TokenType.rollback, InsertMode.insertOrAbort: TokenType.abort, InsertMode.insertOrFail: TokenType.fail, InsertMode.insertOrIgnore: TokenType.ignore, }[mode]!); } keyword(TokenType.into); visit(e.table, arg); if (e.targetColumns.isNotEmpty) { symbol('(', spaceBefore: true); _join(e.targetColumns, ','); symbol(')', spaceAfter: true); } visit(e.source, arg); visitNullable(e.upsert, arg); visitNullable(e.returning, arg); } @override void visitInsertTriggerTarget(InsertTarget e, void arg) { keyword(TokenType.insert); } @override void visitInvalidStatement(InvalidStatement e, void arg) { throw UnsupportedError( 'InvalidStatement does not have a textual representation'); } @override void visitIsExpression(IsExpression e, void arg) { visit(e.left, arg); keyword(TokenType.$is); // Avoid writing `DISTINCT FROM`, but be aware that it effectively negates // the generated `IS` again. final negated = e.negated ^ e.distinctFromSyntax; if (negated) { keyword(TokenType.not); } visit(e.right, arg); } @override void visitIsNullExpression(IsNullExpression e, void arg) { visit(e.operand, arg); if (e.negated) { keyword(TokenType.notNull); } else { keyword(TokenType.isNull); } } @override void visitJoin(Join e, void arg) { visit(e.operator, null); visit(e.query, null); final constraint = e.constraint; if (constraint is OnConstraint) { keyword(TokenType.on); visit(constraint.expression, arg); } else if (constraint is UsingConstraint) { keyword(TokenType.using); symbol('(${constraint.columnNames.join(', ')})'); } } @override void visitJoinOperator(JoinOperator e, void arg) { if (e.operator == JoinOperatorKind.comma) { symbol(','); } else { if (e.natural) { keyword(TokenType.natural); } switch (e.operator) { case JoinOperatorKind.none: break; case JoinOperatorKind.comma: throw AssertionError("Can't happen"); case JoinOperatorKind.left: keyword(TokenType.left); break; case JoinOperatorKind.right: keyword(TokenType.right); break; case JoinOperatorKind.full: keyword(TokenType.full); break; case JoinOperatorKind.inner: keyword(TokenType.inner); break; case JoinOperatorKind.cross: keyword(TokenType.cross); break; } if (e.outer) { keyword(TokenType.outer); } keyword(TokenType.join); } } @override void visitJoinClause(JoinClause e, void arg) { visit(e.primary, arg); visitList(e.joins, arg); } @override void visitLimit(Limit e, void arg) { keyword(TokenType.limit); visit(e.count, arg); if (e.offset != null) { keyword(TokenType.offset); visit(e.offset!, arg); } } @override void visitNamedVariable(ColonNamedVariable e, void arg) { // Note: The name already starts with the colon symbol(e.name, spaceBefore: true, spaceAfter: true); } @override void visitNullLiteral(NullLiteral e, void arg) { keyword(TokenType.$null); } @override void visitNumberedVariable(NumberedVariable e, void arg) { symbol('?', spaceBefore: true, spaceAfter: e.explicitIndex == null); if (e.explicitIndex != null) { symbol(e.explicitIndex.toString(), spaceAfter: true); } } @override void visitNumericLiteral(NumericLiteral e, void arg) { symbol(e.value.toString(), spaceBefore: true, spaceAfter: true); } @override void visitOrderBy(OrderBy e, void arg) { keyword(TokenType.order); keyword(TokenType.by); _join(e.terms, ','); } @override void visitOrderingTerm(OrderingTerm e, void arg) { visit(e.expression, arg); _orderingMode(e.orderingMode); if (e.nulls != null) { keyword(TokenType.nulls); keyword(const { OrderingBehaviorForNulls.first: TokenType.first, OrderingBehaviorForNulls.last: TokenType.last, }[e.nulls!]!); } } @override void visitParentheses(Parentheses e, void arg) { symbol('('); visit(e.expression, arg); symbol(')'); } @override void visitRaiseExpression(RaiseExpression e, void arg) { keyword(TokenType.raise); symbol('(', spaceBefore: true); keyword(const { RaiseKind.ignore: TokenType.ignore, RaiseKind.rollback: TokenType.rollback, RaiseKind.abort: TokenType.abort, RaiseKind.fail: TokenType.fail, }[e.raiseKind]!); if (e.errorMessage != null) { symbol(',', spaceAfter: true); _stringLiteral(e.errorMessage!); } symbol(')', spaceAfter: true); } @override void visitReference(Reference e, void arg) { var didWriteSpaceBefore = false; if (e.schemaName != null) { identifier(e.schemaName!, spaceAfter: false); symbol('.'); didWriteSpaceBefore = true; } if (e.entityName != null) { identifier(e.entityName!, spaceAfter: false, spaceBefore: !didWriteSpaceBefore); symbol('.'); didWriteSpaceBefore = true; } identifier(e.columnName, spaceAfter: true, spaceBefore: !didWriteSpaceBefore); } @override void visitReturning(Returning e, void arg) { keyword(TokenType.returning); _join(e.columns, ','); } @override void visitSelectInsertSource(SelectInsertSource e, void arg) { visit(e.stmt, arg); } @override void visitSelectStatement(SelectStatement e, void arg) { visitNullable(e.withClause, arg); keyword(TokenType.select); if (e.distinct) { keyword(TokenType.distinct); } _join(e.columns, ','); _from(e.from); _where(e.where); visitNullable(e.groupBy, arg); if (e.windowDeclarations.isNotEmpty) { keyword(TokenType.window); var isFirst = true; for (final declaration in e.windowDeclarations) { if (!isFirst) { symbol(',', spaceAfter: true); } identifier(declaration.name); keyword(TokenType.as); visit(declaration.definition, arg); isFirst = false; } } visitNullable(e.orderBy, arg); visitNullable(e.limit, arg); } @override void visitSelectStatementAsSource(SelectStatementAsSource e, void arg) { symbol('(', spaceBefore: true); visit(e.statement, arg); symbol(')', spaceAfter: true); if (e.as != null) { keyword(TokenType.as); identifier(e.as!); } } @override void visitSemicolonSeparatedStatements( SemicolonSeparatedStatements e, void arg) { for (final stmt in e.statements) { visit(stmt, arg); buffer.writeln(';'); needsSpace = false; } } @override void visitSingleColumnSetComponent(SingleColumnSetComponent e, void arg) { visit(e.column, arg); symbol('=', spaceBefore: true, spaceAfter: true); visit(e.expression, arg); } @override void visitMultiColumnSetComponent(MultiColumnSetComponent e, void arg) { symbol('(', spaceBefore: true); _join(e.columns, ','); symbol(')'); symbol('=', spaceBefore: true, spaceAfter: true); visit(e.rowValue, arg); } @override void visitStarFunctionParameter(StarFunctionParameter e, void arg) { symbol('*', spaceAfter: true); } @override void visitStarResultColumn(StarResultColumn e, void arg) { if (e.tableName != null) { identifier(e.tableName!); symbol('.'); } symbol('*', spaceAfter: true, spaceBefore: e.tableName == null); } @override void visitStringComparison(StringComparisonExpression e, void arg) { visit(e.left, arg); if (e.not) { keyword(TokenType.not); } keyword(e.operator.type); visit(e.right, arg); if (e.escape != null) { keyword(TokenType.escape); visit(e.escape!, arg); } } @override void visitStringLiteral(StringLiteral e, void arg) { if (e.isBinary) { symbol('X', spaceBefore: true); } _stringLiteral(e.value); } @override void visitSubQuery(SubQuery e, void arg) { symbol('(', spaceBefore: true); visit(e.select, arg); symbol(')', spaceAfter: true); } @override void visitTableConstraint(TableConstraint e, void arg) { if (e.name != null) { keyword(TokenType.constraint); identifier(e.name!); } if (e is KeyClause) { if (e.isPrimaryKey) { keyword(TokenType.primary); keyword(TokenType.key); } else { keyword(TokenType.unique); } symbol('('); _join(e.columns, ','); symbol(')'); _conflictClause(e.onConflict); } else if (e is CheckTable) { keyword(TokenType.check); symbol('('); visit(e.expression, arg); symbol(')'); } else if (e is ForeignKeyTableConstraint) { keyword(TokenType.foreign); keyword(TokenType.key); symbol('('); _join(e.columns, ','); symbol(')'); visit(e.clause, arg); } } @override void visitTableReference(TableReference e, void arg) { if (e.schemaName != null) { identifier(e.schemaName!, spaceAfter: false); symbol('.'); } identifier(e.tableName, spaceBefore: e.schemaName == null); if (e.as != null) { keyword(TokenType.as); identifier(e.as!); } } @override void visitTableValuedFunction(TableValuedFunction e, void arg) { identifier(e.name); symbol('('); visit(e.parameters, arg); symbol(')'); if (e.as != null) { keyword(TokenType.as); identifier(e.as!); } } @override void visitTimeConstantLiteral(TimeConstantLiteral e, void arg) { switch (e.kind) { case TimeConstantKind.currentTime: keyword(TokenType.currentTime); break; case TimeConstantKind.currentDate: keyword(TokenType.currentDate); break; case TimeConstantKind.currentTimestamp: keyword(TokenType.currentTimestamp); break; } } @override void visitTuple(Tuple e, void arg) { symbol('(', spaceBefore: true); _join(e.expressions, ','); symbol(')', spaceAfter: true); } @override void visitUnaryExpression(UnaryExpression e, void arg) { switch (e.operator.type) { case TokenType.minus: symbol('-', spaceBefore: true); break; case TokenType.plus: symbol('+', spaceBefore: true); break; case TokenType.tilde: symbol('~', spaceBefore: true); break; case TokenType.not: keyword(TokenType.not); break; default: throw AssertionError('Unknown unary operator: ${e.operator}'); } visit(e.inner, arg); } @override void visitUpdateStatement(UpdateStatement e, void arg) { visitNullable(e.withClause, arg); keyword(TokenType.update); if (e.or != null) { keyword(TokenType.or); keyword(const { FailureMode.rollback: TokenType.rollback, FailureMode.abort: TokenType.abort, FailureMode.replace: TokenType.replace, FailureMode.fail: TokenType.fail, FailureMode.ignore: TokenType.ignore, }[e.or!]!); } visit(e.table, arg); keyword(TokenType.set); _join(e.set, ','); _from(e.from); _where(e.where); visitNullable(e.returning, arg); } @override void visitUpdateTriggerTarget(UpdateTarget e, void arg) { keyword(TokenType.update); if (e.columnNames.isNotEmpty) { keyword(TokenType.of); _join(e.columnNames, ','); } } @override void visitUpsertClause(UpsertClause e, void arg) { _join(e.entries, ''); } @override void visitUpsertClauseEntry(UpsertClauseEntry e, void arg) { keyword(TokenType.on); keyword(TokenType.conflict); if (e.onColumns != null) { symbol('(', spaceBefore: true); _join(e.onColumns!, ','); symbol(')', spaceAfter: true); _where(e.where); } keyword(TokenType.$do); visit(e.action, arg); } @override void visitValuesSelectStatement(ValuesSelectStatement e, void arg) { keyword(TokenType.$values); _join(e.values, ','); } @override void visitValuesSource(ValuesSource e, void arg) { keyword(TokenType.$values); _join(e.values, ','); } @override void visitWhen(WhenComponent e, void arg) { keyword(TokenType.when); visit(e.when, arg); keyword(TokenType.then); visit(e.then, arg); } @override void visitWindowDefinition(WindowDefinition e, void arg) { symbol('(', spaceBefore: true); if (e.baseWindowName != null) { identifier(e.baseWindowName!); } if (e.partitionBy.isNotEmpty) { keyword(TokenType.partition); keyword(TokenType.by); _join(e.partitionBy, ','); } visitNullable(e.orderBy, arg); visitNullable(e.frameSpec, arg); symbol(')', spaceAfter: true); } @override void visitWithClause(WithClause e, void arg) { keyword(TokenType.$with); if (e.recursive) { keyword(TokenType.recursive); } _join(e.ctes, ','); } void _conflictClause(ConflictClause? clause) { if (clause != null) { keyword(TokenType.on); keyword(TokenType.conflict); keyword(const { ConflictClause.rollback: TokenType.rollback, ConflictClause.abort: TokenType.abort, ConflictClause.fail: TokenType.fail, ConflictClause.ignore: TokenType.ignore, ConflictClause.replace: TokenType.replace, }[clause]!); } } void _from(Queryable? from) { if (from != null) { keyword(TokenType.from); visit(from, null); } } /// Writes an identifier, escaping it if necessary. void identifier(String identifier, {bool spaceBefore = true, bool spaceAfter = true}) { if (isKeyword(identifier) || _notAKeywordRegex.hasMatch(identifier)) { identifier = escapeIdentifier(identifier); } symbol(identifier, spaceBefore: spaceBefore, spaceAfter: spaceAfter); } void dartCode(String code, {bool spaceBefore = true, bool spaceAfter = true}) { symbol('`$code`', spaceBefore: spaceBefore, spaceAfter: spaceAfter); } void _ifNotExists(bool ifNotExists) { if (ifNotExists) { keyword(TokenType.$if); keyword(TokenType.not); keyword(TokenType.exists); } } void _join(Iterable nodes, String separatingSymbol) { var isFirst = true; for (final node in nodes) { if (!isFirst) { symbol(separatingSymbol, spaceAfter: true); } visit(node, null); isFirst = false; } } void keyword(TokenType type) { symbol(reverseKeywords[type]!, spaceAfter: true, spaceBefore: true); } void _orderingMode(OrderingMode? mode) { if (mode != null) { keyword(const { OrderingMode.ascending: TokenType.asc, OrderingMode.descending: TokenType.desc, }[mode]!); } } void _space() => buffer.writeCharCode($space); void _stringLiteral(String content) { final escapedChars = content.replaceAll("'", "''"); symbol("'$escapedChars'", spaceBefore: true, spaceAfter: true); } /// Writes the [lexeme], unchanged. void symbol(String lexeme, {bool spaceBefore = false, bool spaceAfter = false}) { if (needsSpace && spaceBefore) { _space(); } buffer.write(lexeme); needsSpace = spaceAfter; } void _where(Expression? where) { if (where != null) { keyword(TokenType.where); visit(where, null); } } } /// Defines the [toSql] extension method that turns ast nodes into a compatible /// textual representation. /// /// Parsing the output of [toSql] will result in an equal AST. extension NodeToText on AstNode { /// Obtains a textual representation for AST nodes. /// /// Parsing the output of [toSql] will result in an equal AST. Since only the /// AST is used, the output will not contain comments. It's possible for the /// output to have more than just whitespace changes if there are multiple /// ways to represent an equivalent node (e.g. the no-op `FOR EACH ROW` on /// triggers). String toSql() { final builder = NodeSqlBuilder(null); builder.visit(this, null); return builder.buffer.toString(); } } // Allow 0, 1, 2, 3 in https://github.com/sqlite/sqlite/blob/665b6b6b35f10a46bb72377ade7c2e5d8ea42cb3/src/tokenize.c#L62-L80 final _notAKeywordRegex = RegExp('[^A-Za-z_0-9]');