part of 'dsl.dart';

/// Base class for dsl [Table]s and [View]s.
abstract class HasResultSet {
  /// Default constant constructor.
  const HasResultSet();
}

/// Subclasses represent a table in a database generated by drift.
///
/// For more information on how to write tables, see [the documentation](https://drift.simonbinder.eu/docs/getting-started/advanced_dart_tables/)
abstract class Table extends HasResultSet {
  /// Defines a table to be used with drift.
  const Table();

  /// The sql table name to be used. By default, drift will use the snake_case
  /// representation of your class name as the sql table name. For instance, a
  /// [Table] class named `LocalSettings` will be called `local_settings` by
  /// default.
  /// You can change that behavior by overriding this method to use a custom
  /// name. Please note that you must directly return a string literal by using
  /// a getter. For instance `@override String get tableName => 'my_table';` is
  /// valid, whereas `@override final String tableName = 'my_table';` or
  /// `@override String get tableName => createMyTableName();` is not.
  @visibleForOverriding
  String? get tableName => null;

  /// Whether to append a `WITHOUT ROWID` clause in the `CREATE TABLE`
  /// statement. This is intended to be used by generated code only.
  bool get withoutRowId => false;

  /// Drift will write some table constraints automatically, for instance when
  /// you override [primaryKey]. You can turn this behavior off if you want to.
  /// This is intended to be used by generated code only.
  bool get dontWriteConstraints => false;

  /// Whether this table is `STRICT`.
  ///
  /// Strict tables enforce stronger type constraints for inserts and updates.
  /// Support for strict tables was added in sqlite3 version 37.
  /// This field is intended to be used by generated code only.
  bool get isStrict => false;

  /// Override this to specify custom primary keys:
  /// ```dart
  /// class IngredientInRecipes extends Table {
  ///  @override
  ///  Set<Column> get primaryKey => {recipe, ingredient};
  ///
  ///  IntColumn get recipe => integer()();
  ///  IntColumn get ingredient => integer()();
  ///
  ///  IntColumn get amountInGrams => integer().named('amount')();
  ///}
  /// ```
  /// The getter must return a set literal using the `=>` syntax so that the
  /// drift generator can understand the code.
  /// Also, please note that it's an error to have an
  /// [BuildIntColumn.autoIncrement] column and a custom primary key.
  /// As an auto-incremented `IntColumn` is recognized by drift to be the
  /// primary key, doing so will result in an exception thrown at runtime.
  @visibleForOverriding
  Set<Column>? get primaryKey => null;

  /// Unique constraints in this table.
  ///
  /// When two rows have the same value in _any_  set specified in [uniqueKeys],
  /// the database will reject the second row for inserts.
  ///
  /// Override this to specify unique keys:
  ///
  /// ```dart
  /// class IngredientInRecipes extends Table {
  ///  @override
  ///  List<Set<Column>> get uniqueKeys =>
  ///     [{recipe, ingredient}, {recipe, amountInGrams}];
  ///
  ///  IntColumn get recipe => integer()();
  ///  IntColumn get ingredient => integer()();
  ///
  ///  IntColumn get amountInGrams => integer().named('amount')();
  /// ```
  ///
  /// The getter must return a list of set literals using the `=>` syntax so
  /// that the drift generator can understand the code.
  ///
  /// Note that individual columns can also be marked as unique with
  /// [BuildGeneralColumn.unique]. This is equivalent to adding a single-element
  /// set to this list.
  @visibleForOverriding
  List<Set<Column>>? get uniqueKeys => null;

  /// Custom table constraints that should be added to the table.
  ///
  /// See also:
  ///  - https://www.sqlite.org/syntax/table-constraint.html, which defines what
  ///    table constraints are supported.
  List<String> get customConstraints => [];

  /// Use this as the body of a getter to declare a column that holds integers.
  ///
  /// Example (inside the body of a table class):
  /// ```
  /// IntColumn get id => integer().autoIncrement()();
  /// ```
  ///
  /// In sqlite3, an integer column stores 64-big integers. This column maps
  /// values to an [int] in Dart, which works well on native platforms. On the
  /// web, be aware that [int]s are [double]s internally which means that only
  /// integers smaller than 2⁵² can safely be stored.
  /// If you need web support __and__ a column that potential stores integers
  /// larger than what fits into 52 bits, consider using a [int64] column
  /// instead. That column stores the same value in a database, but makes drift
  /// report the values as a [BigInt] in Dart.

  @protected
  ColumnBuilder<int> integer() => _isGenerated();

  /// Use this as the body of a getter to declare a column that holds a 64-big
  /// integer as a [BigInt].
  ///
  /// The main purpose of this column is to support large integers for web apps
  /// compiled to JavaScript, where using an [int] does not reliably work for
  /// numbers larger than 2⁵².
  /// It stores the exact same data as an [integer] column (and supports the
  /// same options), but instructs drift to generate a data class with a
  /// [BigInt] field and a database conversion aware of large intergers.
  ///
  /// __Note__: The use of [int64] is only necessary for apps that need to work
  /// on the web __and__ use columns that are likely to store values larger than
  /// 2⁵². In all other cases, using [integer] directly is much more efficient
  /// and recommended.
  @protected
  ColumnBuilder<BigInt> int64() => _isGenerated();

  /// Creates a column to store an `enum` class [T].
  ///
  /// In the database, the column will be represented as an integer
  /// corresponding to the enum's index. Note that this can invalidate your data
  /// if you add another value to the enum class.
  @protected
  ColumnBuilder<int> intEnum<T extends Enum>() => _isGenerated();

  /// Use this as the body of a getter to declare a column that holds strings.
  /// Example (inside the body of a table class):
  /// ```
  /// TextColumn get name => text()();
  /// ```
  @protected
  ColumnBuilder<String> text() => _isGenerated();

  /// Creates a column to store an `enum` class [T].
  ///
  /// In the database, the column will be represented as text corresponding to
  /// the name of the enum entries. Note that this can invalidate your data if
  /// you rename the entries of the enum class.
  ColumnBuilder<String> textEnum<T extends Enum>() => _isGenerated();

  /// Use this as the body of a getter to declare a column that holds bools.
  /// Example (inside the body of a table class):
  /// ```
  /// BoolColumn get isAwesome => boolean()();
  /// ```
  @protected
  ColumnBuilder<bool> boolean() => _isGenerated();

  /// Use this as the body of a getter to declare a column that holds date and
  /// time.
  ///
  /// Drift supports two modes for storing date times: As unix timestamp with
  /// second accuracy (the default) and as ISO 8601 string with microsecond
  /// accuracy. For more information between the modes, and information on how
  /// to change them, see [the documentation].
  ///
  /// Note that [DateTime] values are stored on a second-accuracy.
  /// Example (inside the body of a table class):
  /// ```
  /// DateTimeColumn get accountCreatedAt => dateTime()();
  /// ```
  /// [the documentation]: https://drift.simonbinder.eu/docs/getting-started/advanced_dart_tables/#supported-column-types
  @protected
  ColumnBuilder<DateTime> dateTime() => _isGenerated();

  /// Use this as the body of a getter to declare a column that holds arbitrary
  /// data blobs, stored as an [Uint8List]. Example:
  /// ```
  /// BlobColumn get payload => blob()();
  /// ```
  @protected
  ColumnBuilder<Uint8List> blob() => _isGenerated();

  /// Use this as the body of a getter to declare a column that holds floating
  /// point numbers. Example
  /// ```
  /// RealColumn get averageSpeed => real()();
  /// ```
  @protected
  ColumnBuilder<double> real() => _isGenerated();

  /// Defines a column with a custom [type] when used as a getter.
  ///
  /// For more information on custom types and when they can be useful, see
  /// https://drift.simonbinder.eu/docs/sql-api/types/.
  ///
  /// For most users, [TypeConverter]s are a more appropriate tool to store
  /// custom values in the database.
  @protected
  ColumnBuilder<T> customType<T extends Object>(CustomSqlType<T> type) =>
      _isGenerated();
}

/// Subclasses represent a view in a database generated by drift.
///
/// For more information on how to define views in Dart, see
/// [the documentation](https://drift.simonbinder.eu/docs/getting-started/advanced_dart_tables/#views)
abstract class View extends HasResultSet {
  /// Defines a view to be used with drift.
  const View();

  /// The select method can be used in [as] to define the select query backing
  /// this view.
  ///
  /// The select statement should select all columns defined on this view.
  @protected
  View select(List<Expression> columns) => _isGenerated();

  /// This method should be called on [select] to define the main table of this
  /// view:
  ///
  /// ```dart
  /// abstract class CategoryTodoCount extends View {
  ///   TodosTable get todos;
  ///   Categories get categories;
  ///
  ///   Expression<int> get itemCount => todos.id.count();
  ///
  ///   @override
  ///   Query as() => select([categories.description, itemCount])
  ///       .from(categories)
  ///       .join([innerJoin(todos, todos.category.equalsExp(categories.id))]);
  /// }
  /// ```
  @protected
  SimpleSelectStatement from(Table table) => _isGenerated();

  /// This method is overridden by Dart-defined views to declare the right
  /// query to run.
  @visibleForOverriding
  Query as();
}

/// Annotations for Dart table classes to define a [SQL index](https://sqlite.org/lang_createindex.html)
/// to add to the table.
///
/// ```dart
/// @TableIndex(name: 'item_title', columns: {#title})
/// class TodoItems extends Table {
///   IntColumn get id => integer().autoIncrement()();
///   TextColumn get title => text()();
///   TextColumn get content => text().nullable()();
/// }
/// ```
@Target({TargetKind.classType})
final class TableIndex {
  /// The name of the index in SQL.
  ///
  /// Please note that the name of every table, view, index and other elements
  /// in a database must be unique. For this reason, the names of indices are
  /// commonly prefixed with the name of the table they're referencing.
  final String name;

  /// Whether this index is `UNIQUE`, meaning that the database will forbid
  /// multiple rows in the annotated table from having the same values in the
  /// indexed columns.
  final bool unique;

  /// The columns of the table that should be part of the index.
  ///
  /// Columns are referenced with a [Symbol] of their getter name used in the
  /// column definition. For instance, a table declaring a column as
  /// `IntColumn get nextUpdateSnapshot => ...()` could reference this column
  /// using `#nextUpdateSnapshot`.
  final Set<Symbol> columns;

  /// An annotation for Dart-defined drift tables telling drift to add an SQL
  /// index to the table.
  ///
  /// See the class documentation at [TableIndex] for an example.
  const TableIndex({
    required this.name,
    required this.columns,
    this.unique = false,
  });
}

/// A class to be used as an annotation on [Table] classes to customize the
/// name for the data class that will be generated for the table class. The data
/// class is a dart object that will be used to represent a row in the table.
///
/// {@template drift_custom_data_class}
/// By default, drift will attempt to use the singular form of the table name
/// when naming data classes (e.g. a table named "Users" will generate a data
/// class called "User"). However, this doesn't work for irregular plurals and
/// you might want to choose a different name, for which this annotation can be
/// used.
/// {@endtemplate}
@Target({TargetKind.classType})
class DataClassName {
  /// The overridden name to use when generating the data class for a table.
  /// {@macro drift_custom_data_class}
  final String name;

  /// The parent type of the data class generated by drift.
  ///
  /// The [extending] type must refer to an interface type (usually just a
  /// class name), and the parent class must extend [DataClass].
  ///
  /// The extended class can optionally have a type parameter, which is
  /// instantiated to the actual data class generated by drift.
  ///
  /// For example,
  ///
  /// ```dart
  ///  abstract class BaseModel extends DataClass {
  ///    abstract final String id;
  ///  }
  ///
  ///  abstract class TypedBaseModel<T> extends DataClass {
  ///
  ///  }
  ///
  ///  @DataClassName('Company', extending: BaseModel)
  ///  class Companies extends Table {
  ///    TextColumn get id => text()();
  ///    TextColumn get name => text().named('name')();
  ///  }
  ///
  ///  // The actual generated class will extend `TypedBaseModel<Employee>`.
  ///  @DataClassName('Employee', extending: TypedBaseModel)
  ///  class Employees extends Table {
  ///    TextColumn get id => text()();
  ///  }
  /// ```
  final Type? extending;

  /// Customize the data class name for a given table.
  /// {@macro drift_custom_data_class}
  const DataClassName(this.name, {this.extending});
}

/// An annotation specifying an existing class to be used as a data class.
@Target({TargetKind.classType})
class UseRowClass {
  /// The existing class
  ///
  /// This type must refer to an existing class. All other types, like functions
  /// or types with arguments, are not allowed.
  final Type type;

  /// The name of the constructor to use.
  ///
  /// When this option is not set, the default (unnamed) constructor will be
  /// used to map database rows to the desired row class.
  final String constructor;

  /// Generate a `toInsertable()` extension function for [type] mapping all
  /// fields to an insertable object.
  ///
  /// This can be useful when a custom data class should be used for inserts or
  /// updates.
  @Deprecated('Use `write_to_columns_mixins` build option instead')
  final bool generateInsertable;

  /// Customize the class used by drift to hold an instance of an annotated
  /// table.
  ///
  /// For details, see the overall documentation on [UseRowClass].
  const UseRowClass(this.type,
      {this.constructor = '', this.generateInsertable = false});
}

/// An annotation specifying view properties
@Target({TargetKind.classType})
class DriftView {
  /// The sql view name to be used. By default, drift will use the snake_case
  /// representation of your class name as the sql view name. For instance, a
  /// [View] class named `UserView` will be called `user_view` by
  /// default.
  final String? name;

  /// The name for the data class that will be generated for the view class.
  /// The data class is a dart object that will be used to represent a result of
  /// the view.
  /// {@template drift_custom_data_class}
  /// By default, drift will attempt to use the view name followed by "Data"
  /// when naming data classes (e.g. a view named "UserView" will generate a
  /// data class called "UserViewData").
  /// {@macro drift_custom_data_class}
  final String? dataClassName;

  /// The parent class of generated data class. Class must extends [DataClass]!
  final Type? extending;

  /// Customize view name and data class name
  const DriftView({this.name, this.dataClassName, this.extending});
}
