Add 'packages/orm/' from commit 'ceb58a25b58eeabeeab5a0bb6257f144e150dc24'

git-subtree-dir: packages/orm
git-subtree-mainline: edfd785dfe
git-subtree-split: ceb58a25b5
This commit is contained in:
Tobe O 2020-02-15 18:22:15 -05:00
commit 6db839928b
185 changed files with 16826 additions and 0 deletions

57
packages/orm/.gitignore vendored Normal file
View file

@ -0,0 +1,57 @@
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
.packages
.pub/
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
.dart_tool

1
packages/orm/.idea/.name Normal file
View file

@ -0,0 +1 @@
orm

View file

@ -0,0 +1,451 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DBNavigator.Project.DataEditorManager">
<record-view-column-sorting-type value="BY_INDEX" />
<value-preview-text-wrapping value="true" />
<value-preview-pinned value="false" />
</component>
<component name="DBNavigator.Project.DataExportManager">
<export-instructions>
<create-header value="true" />
<quote-values-containing-separator value="true" />
<quote-all-values value="false" />
<value-separator value="" />
<file-name value="" />
<file-location value="" />
<scope value="GLOBAL" />
<destination value="FILE" />
<format value="EXCEL" />
<charset value="windows-1252" />
</export-instructions>
</component>
<component name="DBNavigator.Project.DatabaseBrowserManager">
<autoscroll-to-editor value="false" />
<autoscroll-from-editor value="true" />
<show-object-properties value="true" />
<loaded-nodes />
</component>
<component name="DBNavigator.Project.EditorStateManager">
<last-used-providers />
</component>
<component name="DBNavigator.Project.MethodExecutionManager">
<method-browser />
<execution-history>
<group-entries value="true" />
<execution-inputs />
</execution-history>
<argument-values-cache />
</component>
<component name="DBNavigator.Project.ObjectDependencyManager">
<last-used-dependency-type value="INCOMING" />
</component>
<component name="DBNavigator.Project.ObjectQuickFilterManager">
<last-used-operator value="EQUAL" />
<filters />
</component>
<component name="DBNavigator.Project.ScriptExecutionManager" clear-outputs="true">
<recently-used-interfaces />
</component>
<component name="DBNavigator.Project.Settings">
<connections />
<browser-settings>
<general>
<display-mode value="TABBED" />
<navigation-history-size value="100" />
<show-object-details value="false" />
</general>
<filters>
<object-type-filter>
<object-type name="SCHEMA" enabled="true" />
<object-type name="USER" enabled="true" />
<object-type name="ROLE" enabled="true" />
<object-type name="PRIVILEGE" enabled="true" />
<object-type name="CHARSET" enabled="true" />
<object-type name="TABLE" enabled="true" />
<object-type name="VIEW" enabled="true" />
<object-type name="MATERIALIZED_VIEW" enabled="true" />
<object-type name="NESTED_TABLE" enabled="true" />
<object-type name="COLUMN" enabled="true" />
<object-type name="INDEX" enabled="true" />
<object-type name="CONSTRAINT" enabled="true" />
<object-type name="DATASET_TRIGGER" enabled="true" />
<object-type name="DATABASE_TRIGGER" enabled="true" />
<object-type name="SYNONYM" enabled="true" />
<object-type name="SEQUENCE" enabled="true" />
<object-type name="PROCEDURE" enabled="true" />
<object-type name="FUNCTION" enabled="true" />
<object-type name="PACKAGE" enabled="true" />
<object-type name="TYPE" enabled="true" />
<object-type name="TYPE_ATTRIBUTE" enabled="true" />
<object-type name="ARGUMENT" enabled="true" />
<object-type name="DIMENSION" enabled="true" />
<object-type name="CLUSTER" enabled="true" />
<object-type name="DBLINK" enabled="true" />
</object-type-filter>
</filters>
<sorting>
<object-type name="COLUMN" sorting-type="NAME" />
<object-type name="FUNCTION" sorting-type="NAME" />
<object-type name="PROCEDURE" sorting-type="NAME" />
<object-type name="ARGUMENT" sorting-type="POSITION" />
</sorting>
<default-editors>
<object-type name="VIEW" editor-type="SELECTION" />
<object-type name="PACKAGE" editor-type="SELECTION" />
<object-type name="TYPE" editor-type="SELECTION" />
</default-editors>
</browser-settings>
<navigation-settings>
<lookup-filters>
<lookup-objects>
<object-type name="SCHEMA" enabled="true" />
<object-type name="USER" enabled="false" />
<object-type name="ROLE" enabled="false" />
<object-type name="PRIVILEGE" enabled="false" />
<object-type name="CHARSET" enabled="false" />
<object-type name="TABLE" enabled="true" />
<object-type name="VIEW" enabled="true" />
<object-type name="MATERIALIZED VIEW" enabled="true" />
<object-type name="NESTED TABLE" enabled="false" />
<object-type name="COLUMN" enabled="false" />
<object-type name="INDEX" enabled="true" />
<object-type name="CONSTRAINT" enabled="true" />
<object-type name="DATASET TRIGGER" enabled="true" />
<object-type name="DATABASE TRIGGER" enabled="true" />
<object-type name="SYNONYM" enabled="false" />
<object-type name="SEQUENCE" enabled="true" />
<object-type name="PROCEDURE" enabled="true" />
<object-type name="FUNCTION" enabled="true" />
<object-type name="PACKAGE" enabled="true" />
<object-type name="TYPE" enabled="true" />
<object-type name="TYPE ATTRIBUTE" enabled="false" />
<object-type name="ARGUMENT" enabled="false" />
<object-type name="DIMENSION" enabled="false" />
<object-type name="CLUSTER" enabled="false" />
<object-type name="DBLINK" enabled="true" />
</lookup-objects>
<force-database-load value="false" />
<prompt-connection-selection value="true" />
<prompt-schema-selection value="true" />
</lookup-filters>
</navigation-settings>
<dataset-grid-settings>
<general>
<enable-zooming value="true" />
</general>
<sorting>
<nulls-first value="true" />
<max-sorting-columns value="4" />
</sorting>
<tracking-columns>
<columnNames value="" />
<visible value="true" />
<editable value="false" />
</tracking-columns>
</dataset-grid-settings>
<dataset-editor-settings>
<text-editor-popup>
<active value="false" />
<active-if-empty value="false" />
<data-length-threshold value="100" />
<popup-delay value="1000" />
</text-editor-popup>
<values-list-popup>
<show-popup-button value="true" />
<element-count-threshold value="1000" />
<data-length-threshold value="250" />
</values-list-popup>
<general>
<fetch-block-size value="100" />
<fetch-timeout value="30" />
<trim-whitespaces value="true" />
<convert-empty-strings-to-null value="true" />
<select-content-on-cell-edit value="true" />
<large-value-preview-active value="true" />
</general>
<filters>
<prompt-filter-dialog value="true" />
<default-filter-type value="BASIC" />
</filters>
<qualified-text-editor text-length-threshold="300">
<content-types>
<content-type name="Text" enabled="true" />
<content-type name="XML" enabled="true" />
<content-type name="DTD" enabled="true" />
<content-type name="HTML" enabled="true" />
<content-type name="XHTML" enabled="true" />
<content-type name="CSS" enabled="true" />
<content-type name="SQL" enabled="true" />
<content-type name="PL/SQL" enabled="true" />
<content-type name="JavaScript" enabled="true" />
<content-type name="JSP" enabled="true" />
<content-type name="JSPx" enabled="true" />
<content-type name="ASP" enabled="true" />
<content-type name="YAML" enabled="true" />
<content-type name="Bash" enabled="true" />
</content-types>
</qualified-text-editor>
<record-navigation>
<navigation-target value="VIEWER" />
</record-navigation>
</dataset-editor-settings>
<code-editor-settings>
<general>
<show-object-navigation-gutter value="false" />
<show-spec-declaration-navigation-gutter value="true" />
</general>
<confirmations>
<save-changes value="false" />
<revert-changes value="true" />
</confirmations>
</code-editor-settings>
<code-completion-settings>
<filters>
<basic-filter>
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
<filter-element type="RESERVED_WORD" id="function" selected="true" />
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
<filter-element type="OBJECT" id="schema" selected="true" />
<filter-element type="OBJECT" id="role" selected="true" />
<filter-element type="OBJECT" id="user" selected="true" />
<filter-element type="OBJECT" id="privilege" selected="true" />
<user-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="false" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</user-schema>
<public-schema>
<filter-element type="OBJECT" id="table" selected="false" />
<filter-element type="OBJECT" id="view" selected="false" />
<filter-element type="OBJECT" id="materialized view" selected="false" />
<filter-element type="OBJECT" id="index" selected="false" />
<filter-element type="OBJECT" id="constraint" selected="false" />
<filter-element type="OBJECT" id="trigger" selected="false" />
<filter-element type="OBJECT" id="synonym" selected="false" />
<filter-element type="OBJECT" id="sequence" selected="false" />
<filter-element type="OBJECT" id="procedure" selected="false" />
<filter-element type="OBJECT" id="function" selected="false" />
<filter-element type="OBJECT" id="package" selected="false" />
<filter-element type="OBJECT" id="type" selected="false" />
<filter-element type="OBJECT" id="dimension" selected="false" />
<filter-element type="OBJECT" id="cluster" selected="false" />
<filter-element type="OBJECT" id="dblink" selected="false" />
</public-schema>
<any-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</any-schema>
</basic-filter>
<extended-filter>
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
<filter-element type="RESERVED_WORD" id="function" selected="true" />
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
<filter-element type="OBJECT" id="schema" selected="true" />
<filter-element type="OBJECT" id="user" selected="true" />
<filter-element type="OBJECT" id="role" selected="true" />
<filter-element type="OBJECT" id="privilege" selected="true" />
<user-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</user-schema>
<public-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</public-schema>
<any-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</any-schema>
</extended-filter>
</filters>
<sorting enabled="true">
<sorting-element type="RESERVED_WORD" id="keyword" />
<sorting-element type="RESERVED_WORD" id="datatype" />
<sorting-element type="OBJECT" id="column" />
<sorting-element type="OBJECT" id="table" />
<sorting-element type="OBJECT" id="view" />
<sorting-element type="OBJECT" id="materialized view" />
<sorting-element type="OBJECT" id="index" />
<sorting-element type="OBJECT" id="constraint" />
<sorting-element type="OBJECT" id="trigger" />
<sorting-element type="OBJECT" id="synonym" />
<sorting-element type="OBJECT" id="sequence" />
<sorting-element type="OBJECT" id="procedure" />
<sorting-element type="OBJECT" id="function" />
<sorting-element type="OBJECT" id="package" />
<sorting-element type="OBJECT" id="type" />
<sorting-element type="OBJECT" id="dimension" />
<sorting-element type="OBJECT" id="cluster" />
<sorting-element type="OBJECT" id="dblink" />
<sorting-element type="OBJECT" id="schema" />
<sorting-element type="OBJECT" id="role" />
<sorting-element type="OBJECT" id="user" />
<sorting-element type="RESERVED_WORD" id="function" />
<sorting-element type="RESERVED_WORD" id="parameter" />
</sorting>
<format>
<enforce-code-style-case value="true" />
</format>
</code-completion-settings>
<execution-engine-settings>
<statement-execution>
<fetch-block-size value="100" />
<execution-timeout value="20" />
<debug-execution-timeout value="600" />
<focus-result value="false" />
<prompt-execution value="false" />
</statement-execution>
<script-execution>
<command-line-interfaces />
<execution-timeout value="300" />
</script-execution>
<method-execution>
<execution-timeout value="30" />
<debug-execution-timeout value="600" />
<parameter-history-size value="10" />
</method-execution>
</execution-engine-settings>
<operation-settings>
<transactions>
<uncommitted-changes>
<on-project-close value="ASK" />
<on-disconnect value="ASK" />
<on-autocommit-toggle value="ASK" />
</uncommitted-changes>
<multiple-uncommitted-changes>
<on-commit value="ASK" />
<on-rollback value="ASK" />
</multiple-uncommitted-changes>
</transactions>
<session-browser>
<disconnect-session value="ASK" />
<kill-session value="ASK" />
<reload-on-filter-change value="false" />
</session-browser>
<compiler>
<compile-type value="KEEP" />
<compile-dependencies value="ASK" />
<always-show-controls value="false" />
</compiler>
<debugger>
<debugger-type value="JDBC" />
<use-generic-runners value="true" />
</debugger>
</operation-settings>
<ddl-file-settings>
<extensions>
<mapping file-type-id="VIEW" extensions="vw" />
<mapping file-type-id="TRIGGER" extensions="trg" />
<mapping file-type-id="PROCEDURE" extensions="prc" />
<mapping file-type-id="FUNCTION" extensions="fnc" />
<mapping file-type-id="PACKAGE" extensions="pkg" />
<mapping file-type-id="PACKAGE_SPEC" extensions="pks" />
<mapping file-type-id="PACKAGE_BODY" extensions="pkb" />
<mapping file-type-id="TYPE" extensions="tpe" />
<mapping file-type-id="TYPE_SPEC" extensions="tps" />
<mapping file-type-id="TYPE_BODY" extensions="tpb" />
</extensions>
<general>
<lookup-ddl-files value="true" />
<create-ddl-files value="false" />
<synchronize-ddl-files value="true" />
<use-qualified-names value="false" />
<make-scripts-rerunnable value="true" />
</general>
</ddl-file-settings>
<general-settings>
<regional-settings>
<date-format value="MEDIUM" />
<number-format value="UNGROUPED" />
<locale value="SYSTEM_DEFAULT" />
<use-custom-formats value="false" />
</regional-settings>
<environment>
<environment-types>
<environment-type id="development" name="Development" description="Development environment" color="-2430209/-12296320" readonly-code="false" readonly-data="false" />
<environment-type id="integration" name="Integration" description="Integration environment" color="-2621494/-12163514" readonly-code="true" readonly-data="false" />
<environment-type id="production" name="Production" description="Productive environment" color="-11574/-10271420" readonly-code="true" readonly-data="true" />
<environment-type id="other" name="Other" description="" color="-1576/-10724543" readonly-code="false" readonly-data="false" />
</environment-types>
<visibility-settings>
<connection-tabs value="true" />
<dialog-headers value="true" />
<object-editor-tabs value="true" />
<script-editor-tabs value="false" />
<execution-result-tabs value="true" />
</visibility-settings>
</environment>
</general-settings>
</component>
<component name="DBNavigator.Project.StatementExecutionManager">
<execution-variables />
</component>
</project>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/orm.iml" filepath="$PROJECT_DIR$/.idea/orm.iml" />
</modules>
</component>
</project>

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$/angel_orm">
<excludeFolder url="file://$MODULE_DIR$/angel_orm/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/angel_orm/.pub" />
<excludeFolder url="file://$MODULE_DIR$/angel_orm/build" />
</content>
<content url="file://$MODULE_DIR$/angel_orm_generator">
<excludeFolder url="file://$MODULE_DIR$/angel_orm_generator/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/angel_orm_generator/.pub" />
<excludeFolder url="file://$MODULE_DIR$/angel_orm_generator/build" />
</content>
<content url="file://$MODULE_DIR$/external">
<excludeFolder url="file://$MODULE_DIR$/external/.pub" />
<excludeFolder url="file://$MODULE_DIR$/external/build" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>

View file

@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests in has_one_test" type="DartTestRunConfigurationType" factoryName="Dart Test" folderName="leg" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/angel_orm_generator/test/has_one_test.dart" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

10
packages/orm/.travis.yml Normal file
View file

@ -0,0 +1,10 @@
language: dart
script: bash tool/.travis.sh
before_script:
- psql -c 'create database angel_orm_test;' -U postgres
- psql -c 'create database angel_orm_service_test;' -U postgres
- psql -c "CREATE USER angel_orm WITH PASSWORD 'angel_orm';" -U postgres
services:
- postgresql
addons:
postgresql: "9.4"

267
packages/orm/README.md Normal file
View file

@ -0,0 +1,267 @@
# orm
[![Pub](https://img.shields.io/pub/v/angel_orm.svg)](https://pub.dartlang.org/packages/angel_orm)
[![build status](https://travis-ci.org/angel-dart/orm.svg)](https://travis-ci.org/angel-dart/orm)
Source-generated PostgreSQL ORM for use with the
[Angel framework](https://angel-dart.github.io).
Now you can combine the power and flexibility of Angel with a strongly-typed ORM.
Documentation for migrations can be found here:
https://angel-dart.gitbook.io/angel/v/2.x/orm/migrations
* [Usage](#usage)
* [Model Definitions](#models)
* [MVC Example](#example)
* [Relationships](#relations)
* [Many-to-Many Relationships](#many-to-many-relations)
* [Columns (`@Column(...)`)](#columns)
* [Column Types](#column-types)
* [Indices](#indices)
* [Default Values](#default-values)
# Usage
You'll need these dependencies in your `pubspec.yaml`:
```yaml
dependencies:
angel_orm: ^2.0.0-dev
dev_dependencies:
angel_orm_generator: ^2.0.0-dev
build_runner: ^1.0.0
```
`package:angel_orm_generator` exports a class that you can include
in a `package:build` flow:
* `PostgresOrmGenerator` - Fueled by `package:source_gen`; include this within a `SharedPartBuilder`.
However, it also includes a `build.yaml` that builds ORM files automatically, so you shouldn't
have to do any configuration at all.
# Models
The ORM works best when used with `package:angel_serialize`:
```dart
library angel_orm.test.models.car;
import 'package:angel_migration/angel_migration.dart';
import 'package:angel_model/angel_model.dart';
import 'package:angel_orm/angel_orm.dart';
import 'package:angel_serialize/angel_serialize.dart';
part 'car.g.dart';
@serializable
@orm
abstract class _Car extends Model {
String get make;
String get description;
bool get familyFriendly;
DateTime get recalledAt;
}
// You can disable migration generation.
@Orm(generateMigrations: false)
abstract class _NoMigrations extends Model {}
```
Models can use the `@SerializableField()` annotation; `package:angel_orm` obeys it.
After building, you'll have access to a `Query` class with strongly-typed methods that
allow to run asynchronous queries without a headache.
Remember that if you don't need automatic id-and-date fields, you can
simply just not extend `Model`:
```dart
@Serializable(autoIdAndDateFields: false)
abstract class _ThisIsNotAnAngelModel {
@primaryKey
String get username;
}
```
# Example
MVC just got a whole lot easier:
```dart
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_orm/angel_orm.dart';
import 'car.dart';
import 'car.orm.g.dart';
/// Returns an Angel plug-in that connects to a database, and sets up a controller connected to it...
AngelConfigurer connectToCarsTable(QueryExecutor executor) {
return (Angel app) async {
// Register the connection with Angel's dependency injection system.
//
// This means that we can use it as a parameter in routes and controllers.
app.container.registerSingleton(executor);
// Attach the controller we create below
await app.mountController<CarController>();
};
}
@Expose('/cars')
class CarController extends Controller {
// The `executor` will be injected.
@Expose('/recalled_since_2008')
carsRecalledSince2008(QueryExecutor executor) {
// Instantiate a Car query, which is auto-generated. This class helps us build fluent queries easily.
var query = new CarQuery();
query.where
..familyFriendly.equals(false)
..recalledAt.year.greaterThanOrEqualTo(2008);
// Shorter syntax we could use instead...
query.where.recalledAt.year <= 2008;
// `get()` returns a Future<List<Car>>.
var cars = await query.get(executor);
return cars;
}
@Expose('/create', method: 'POST')
createCar(QueryExecutor executor) async {
// `package:angel_orm` generates a strongly-typed `insert` function on the query class.
// Say goodbye to typos!!!
var query = new CarQuery();
query.values
..familyFriendly = true
..make 'Honda';
var car = query.insert(executor);
// Auto-serialized using code generated by `package:angel_serialize`
return car;
}
}
```
# Relations
`angel_orm` supports the following relationships:
* `@HasOne()` (one-to-one)
* `@HasMany()` (one-to-many)
* `@BelongsTo()` (one-to-one)
* `@ManyToMany()` (many-to-many, using a "pivot" table)
The annotations can be abbreviated with the default options (ex. `@hasOne`), or supplied
with custom parameters (ex. `@HasOne(foreignKey: 'foreign_id')`).
```dart
@serializable
@orm
abstract class _Author extends Model {
@HasMany // Use the defaults, and auto-compute `foreignKey`
List<_Book> books;
// Also supports parameters...
@HasMany(localKey: 'id', foreignKey: 'author_id', cascadeOnDelete: true)
List<_Book> books;
@SerializableField(alias: 'writing_utensil')
@hasOne
_Pen pen;
}
```
The relationships will "just work" out-of-the-box, following any operation. For example,
after fetching an `Author` from the database in the above example, the `books` field would
be populated with a set of deserialized `Book` objects, also fetched from the database.
Relationships use joins when possible, but in the case of `@HasMany()`, two queries are used:
* One to fetch the object itself
* One to fetch a list of related objects
## Many to Many Relations
A many-to-many relationship can now be modeled like so.
`RoleUser` in this case is a pivot table joining `User` and `Role`.
Note that in this case, the models must reference the private classes (`_User`, etc.), because the canonical versions (`User`, etc.) are not-yet-generated:
```dart
@serializable
@orm
abstract class _User extends Model {
String get username;
String get password;
String get email;
@ManyToMany(_RoleUser)
List<_Role> get roles;
}
@serializable
@orm
abstract class _RoleUser {
@belongsTo
_Role get role;
@belongsTo
_User get user;
}
@serializable
@orm
abstract class _Role extends Model {
String name;
@ManyToMany(_RoleUser)
List<_User> get users;
}
```
TLDR:
1. Make a pivot table, C, between two tables, table A and B
2. C should `@belongsTo` both A and B. C *should not* extend `Model`.
3. A should have a field: `@ManyToMany(_C) List<_B> get b;`
4. B should have a field: `@ManyToMany(_C) List<_A> get a;`
Test: https://raw.githubusercontent.com/angel-dart/orm/master/angel_orm_generator/test/many_to_many_test.dart
# Columns
Use a `@Column()` annotation to change how a given field is handled within the ORM.
## Column Types
Using the `@Column()` annotation, it is possible to explicitly declare the data type of any given field:
```dart
@serializable
@orm
abstract class _Foo extends Model {
@Column(type: ColumnType.bigInt)
int bar;
}
```
## Indices
Columns can also have an `index`:
```dart
@serializable
@orm
abstract class _Foo extends Model {
@Column(index: IndexType.primaryKey)
String bar;
}
```
## Default Values
It is also possible to specify the default value of a field.
**Note that this only works with primitive objects.**
If a default value is supplied, the `SqlMigrationBuilder` will include
it in the generated schema. The `PostgresOrmGenerator` ignores default values;
it does not need them to function properly.
```dart
@serializable
@orm
abstract class _Foo extends Model {
@Column(defaultValue: 'baz')
String bar;
}
```

12
packages/orm/angel_migration/.gitignore vendored Executable file
View file

@ -0,0 +1,12 @@
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
.packages
.pub/
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/

View file

@ -0,0 +1,11 @@
# 2.0.0
* Bump to `2.0.0`.
# 2.0.0-rc.0
* Make abstract `Schema.alter` use `MutableTable`.
# 2.0.0-alpha.1
* Changes to work with `package:angel_orm@2.0.0-dev.15`.
# 2.0.0-alpha
Dart 2 update.

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 The Angel Framework
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,2 @@
# migration
A PostgreSQL database migration framework built on Angel's ORM.

View file

@ -0,0 +1,3 @@
analyzer:
strong-mode:
implicit-casts: false

View file

@ -0,0 +1,42 @@
/// These are straightforward migrations.
///
/// You will likely never have to actually write these yourself.
library angel_migration.example.todo;
import 'package:angel_migration/angel_migration.dart';
class UserMigration implements Migration {
@override
void up(Schema schema) {
schema.create('users', (table) {
table
..serial('id').primaryKey()
..varChar('username', length: 32).unique()
..varChar('password')
..boolean('account_confirmed').defaultsTo(false);
});
}
@override
void down(Schema schema) {
schema.drop('users');
}
}
class TodoMigration implements Migration {
@override
void up(Schema schema) {
schema.create('todos', (table) {
table
..serial('id').primaryKey()
..integer('user_id').references('users', 'id').onDeleteCascade()
..varChar('text')
..boolean('completed').defaultsTo(false);
});
}
@override
void down(Schema schema) {
schema.drop('todos');
}
}

View file

@ -0,0 +1,4 @@
export 'src/column.dart';
export 'src/migration.dart';
export 'src/schema.dart';
export 'src/table.dart';

View file

@ -0,0 +1,85 @@
import 'package:angel_orm/angel_orm.dart';
class MigrationColumn extends Column {
final List<MigrationColumnReference> _references = [];
bool _nullable;
IndexType _index;
dynamic _defaultValue;
@override
bool get isNullable => _nullable;
@override
IndexType get indexType => _index;
get defaultValue => _defaultValue;
List<MigrationColumnReference> get externalReferences =>
new List<MigrationColumnReference>.unmodifiable(_references);
MigrationColumn(ColumnType type,
{bool isNullable: true, int length, IndexType indexType, defaultValue})
: super(type: type, length: length) {
_nullable = isNullable;
_index = indexType;
_defaultValue = defaultValue;
}
factory MigrationColumn.from(Column column) => column is MigrationColumn
? column
: new MigrationColumn(column.type,
isNullable: column.isNullable,
length: column.length,
indexType: column.indexType);
MigrationColumn notNull() => this.._nullable = false;
MigrationColumn defaultsTo(value) => this.._defaultValue = value;
MigrationColumn unique() => this.._index = IndexType.unique;
MigrationColumn primaryKey() => this.._index = IndexType.primaryKey;
MigrationColumnReference references(String foreignTable, String foreignKey) {
var ref = new MigrationColumnReference._(foreignTable, foreignKey);
_references.add(ref);
return ref;
}
}
class MigrationColumnReference {
final String foreignTable, foreignKey;
String _behavior;
MigrationColumnReference._(this.foreignTable, this.foreignKey);
String get behavior => _behavior;
StateError _locked() =>
new StateError('Cannot override existing "$_behavior" behavior.');
void onDeleteCascade() {
if (_behavior != null) throw _locked();
_behavior = 'ON DELETE CASCADE';
}
void onUpdateCascade() {
if (_behavior != null) throw _locked();
_behavior = 'ON UPDATE CASCADE';
}
void onNoAction() {
if (_behavior != null) throw _locked();
_behavior = 'ON UPDATE NO ACTION';
}
void onUpdateSetDefault() {
if (_behavior != null) throw _locked();
_behavior = 'ON UPDATE SET DEFAULT';
}
void onUpdateSetNull() {
if (_behavior != null) throw _locked();
_behavior = 'ON UPDATE SET NULL';
}
}

View file

@ -0,0 +1,6 @@
import 'schema.dart';
abstract class Migration {
void up(Schema schema);
void down(Schema schema);
}

View file

@ -0,0 +1,15 @@
import 'table.dart';
abstract class Schema {
void drop(String tableName, {bool cascade: false});
void dropAll(Iterable<String> tableNames, {bool cascade: false}) {
tableNames.forEach((n) => drop(n, cascade: cascade));
}
void create(String tableName, void callback(Table table));
void createIfNotExists(String tableName, void callback(Table table));
void alter(String tableName, void callback(MutableTable table));
}

View file

@ -0,0 +1,46 @@
import 'package:angel_orm/angel_orm.dart';
import 'column.dart';
abstract class Table {
MigrationColumn declareColumn(String name, Column column);
MigrationColumn declare(String name, ColumnType type) =>
declareColumn(name, new MigrationColumn(type));
MigrationColumn serial(String name) => declare(name, ColumnType.serial);
MigrationColumn integer(String name) => declare(name, ColumnType.int);
MigrationColumn float(String name) => declare(name, ColumnType.float);
MigrationColumn numeric(String name) => declare(name, ColumnType.numeric);
MigrationColumn boolean(String name) => declare(name, ColumnType.boolean);
MigrationColumn date(String name) => declare(name, ColumnType.date);
@deprecated
MigrationColumn dateTime(String name) => timeStamp(name, timezone: true);
MigrationColumn timeStamp(String name, {bool timezone: false}) {
if (timezone != true) return declare(name, ColumnType.timeStamp);
return declare(name, ColumnType.timeStampWithTimeZone);
}
MigrationColumn text(String name) => declare(name, ColumnType.text);
MigrationColumn varChar(String name, {int length}) {
if (length == null) return declare(name, ColumnType.varChar);
return declareColumn(
name, new Column(type: ColumnType.varChar, length: length));
}
}
abstract class MutableTable extends Table {
void rename(String newName);
void dropColumn(String name);
void renameColumn(String name, String newName);
void changeColumnType(String name, ColumnType type);
void dropNotNull(String name);
void setNotNull(String name);
}

View file

View file

@ -0,0 +1,9 @@
name: angel_migration
version: 2.0.0
description: Database migration runtime for Angel's ORM. Use this package to define schemas.
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/migration
environment:
sdk: ">=2.0.0-dev <3.0.0"
dependencies:
angel_orm: ^2.0.0-dev

View file

@ -0,0 +1,13 @@
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
.packages
.pub/
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
.dart_tool

View file

@ -0,0 +1,32 @@
# 2.0.0
* Bump to `2.0.0`.
# 2.0.0-beta.1
* Make `reset` reverse migrations.
# 2.0.0-beta.0
* Make `reset` reverse migrations.
# 2.0.0-alpha.5
* Support default values for columns.
# 2.0.0-alpha.4
* Include the names of migration classes when running.
# 2.0.0-alpha.3
* Run migrations in reverse on `rollback`.
# 2.0.0-alpha.2
* Run migrations in reverse on `reset`.
# 2.0.0-alpha.1
* Cast Iterables via `.cast()`, rather than `as`.
# 2.0.0-alpha
* Dart 2 update.
# 1.0.0-alpha+5
`Schema#drop` now has a named `cascade` parameter, of type `bool`.
# 1.0.0-alpha+1
* You can now pass a `connected` parameter.

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 The Angel Framework
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,2 @@
# migration
A PostgreSQL database migration framework built on Angel's ORM.

View file

@ -0,0 +1,3 @@
analyzer:
strong-mode:
implicit-casts: false

View file

@ -0,0 +1,33 @@
import 'package:angel_migration/angel_migration.dart';
import 'package:angel_migration_runner/angel_migration_runner.dart';
import 'package:angel_migration_runner/postgres.dart';
import 'package:angel_orm/angel_orm.dart';
import 'package:postgres/postgres.dart';
import '../../angel_migration/example/todo.dart';
var migrationRunner = new PostgresMigrationRunner(
new PostgreSQLConnection('127.0.0.1', 5432, 'test',
username: 'postgres', password: 'postgres'),
migrations: [
new UserMigration(),
new TodoMigration(),
new FooMigration(),
],
);
main(List<String> args) => runMigrations(migrationRunner, args);
class FooMigration extends Migration {
@override
void up(Schema schema) {
schema.create('foos', (table) {
table
..serial('id').primaryKey()
..varChar('bar', length: 64)
..timeStamp('created_at').defaultsTo(currentTimestamp);
});
}
@override
void down(Schema schema) => schema.drop('foos');
}

View file

@ -0,0 +1,14 @@
-- Generated by running `todo.dart`
CREATE TABLE "users" (
"id" serial,
"username" varchar(32) UNIQUE,
"password" varchar,
"account_confirmed" serial
);
CREATE TABLE "todos" (
"id" serial,
"user_id" int REFERENCES "user"("id") ON DELETE CASCADE,
"text" varchar,
"completed" serial
);

View file

@ -0,0 +1,2 @@
export 'src/cli.dart';
export 'src/runner.dart';

View file

@ -0,0 +1,3 @@
export 'src/postgres/runner.dart';
export 'src/postgres/schema.dart';
export 'src/postgres/table.dart';

View file

@ -0,0 +1,70 @@
import 'dart:async';
import 'package:args/command_runner.dart';
import 'runner.dart';
/// Runs the Angel Migration CLI.
Future runMigrations(MigrationRunner migrationRunner, List<String> args) {
var cmd = new CommandRunner('migration_runner', 'Executes Angel migrations.')
..addCommand(new _UpCommand(migrationRunner))
..addCommand(new _RefreshCommand(migrationRunner))
..addCommand(new _ResetCommand(migrationRunner))
..addCommand(new _RollbackCommand(migrationRunner));
return cmd.run(args).then((_) => migrationRunner.close());
}
class _UpCommand extends Command {
_UpCommand(this.migrationRunner);
String get name => 'up';
String get description => 'Runs outstanding migrations.';
final MigrationRunner migrationRunner;
@override
run() {
return migrationRunner.up();
}
}
class _ResetCommand extends Command {
_ResetCommand(this.migrationRunner);
String get name => 'reset';
String get description => 'Resets the database.';
final MigrationRunner migrationRunner;
@override
run() {
return migrationRunner.reset();
}
}
class _RefreshCommand extends Command {
_RefreshCommand(this.migrationRunner);
String get name => 'refresh';
String get description =>
'Resets the database, and then re-runs all migrations.';
final MigrationRunner migrationRunner;
@override
run() {
return migrationRunner.reset().then((_) => migrationRunner.up());
}
}
class _RollbackCommand extends Command {
_RollbackCommand(this.migrationRunner);
String get name => 'rollback';
String get description => 'Undoes the last batch of migrations.';
final MigrationRunner migrationRunner;
@override
run() {
return migrationRunner.rollback();
}
}

View file

@ -0,0 +1,138 @@
import 'dart:async';
import 'dart:collection';
import 'package:angel_migration/angel_migration.dart';
import 'package:postgres/postgres.dart';
import '../runner.dart';
import '../util.dart';
import 'schema.dart';
class PostgresMigrationRunner implements MigrationRunner {
final Map<String, Migration> migrations = {};
final PostgreSQLConnection connection;
final Queue<Migration> _migrationQueue = new Queue();
bool _connected = false;
PostgresMigrationRunner(this.connection,
{Iterable<Migration> migrations = const [], bool connected: false}) {
if (migrations?.isNotEmpty == true) migrations.forEach(addMigration);
_connected = connected == true;
}
@override
void addMigration(Migration migration) {
_migrationQueue.addLast(migration);
}
Future _init() async {
while (_migrationQueue.isNotEmpty) {
var migration = _migrationQueue.removeFirst();
var path = await absoluteSourcePath(migration.runtimeType);
migrations.putIfAbsent(path.replaceAll("\\", "\\\\"), () => migration);
}
if (!_connected) {
await connection.open();
_connected = true;
}
await connection.execute('''
CREATE TABLE IF NOT EXISTS "migrations" (
id serial,
batch integer,
path varchar,
PRIMARY KEY(id)
);
''');
}
@override
Future up() async {
await _init();
var r = await connection.query('SELECT path from migrations;');
Iterable<String> existing = r.expand((x) => x).cast<String>();
List<String> toRun = [];
migrations.forEach((k, v) {
if (!existing.contains(k)) toRun.add(k);
});
if (toRun.isNotEmpty) {
var r = await connection.query('SELECT MAX(batch) from migrations;');
int curBatch = (r[0][0] ?? 0) as int;
int batch = curBatch + 1;
for (var k in toRun) {
var migration = migrations[k];
var schema = new PostgresSchema();
migration.up(schema);
print('Bringing up "$k"...');
await schema.run(connection).then((_) {
return connection.execute(
'INSERT INTO MIGRATIONS (batch, path) VALUES ($batch, \'$k\');');
});
}
} else {
print('No migrations found to bring up.');
}
}
@override
Future rollback() async {
await _init();
var r = await connection.query('SELECT MAX(batch) from migrations;');
int curBatch = (r[0][0] ?? 0) as int;
r = await connection
.query('SELECT path from migrations WHERE batch = $curBatch;');
Iterable<String> existing = r.expand((x) => x).cast<String>();
List<String> toRun = [];
migrations.forEach((k, v) {
if (existing.contains(k)) toRun.add(k);
});
if (toRun.isNotEmpty) {
for (var k in toRun.reversed) {
var migration = migrations[k];
var schema = new PostgresSchema();
migration.down(schema);
print('Bringing down "$k"...');
await schema.run(connection).then((_) {
return connection
.execute('DELETE FROM migrations WHERE path = \'$k\';');
});
}
} else {
print('No migrations found to roll back.');
}
}
@override
Future reset() async {
await _init();
var r = await connection
.query('SELECT path from migrations ORDER BY batch DESC;');
Iterable<String> existing = r.expand((x) => x).cast<String>();
var toRun = existing.where(migrations.containsKey).toList();
if (toRun.isNotEmpty) {
for (var k in toRun.reversed) {
var migration = migrations[k];
var schema = new PostgresSchema();
migration.down(schema);
print('Bringing down "$k"...');
await schema.run(connection).then((_) {
return connection
.execute('DELETE FROM migrations WHERE path = \'$k\';');
});
}
} else {
print('No migrations found to roll back.');
}
}
@override
Future close() {
return connection.close();
}
}

View file

@ -0,0 +1,58 @@
import 'dart:async';
import 'package:angel_migration/angel_migration.dart';
import 'package:postgres/postgres.dart';
import 'package:angel_migration_runner/src/postgres/table.dart';
class PostgresSchema extends Schema {
final int _indent;
final StringBuffer _buf;
PostgresSchema._(this._buf, this._indent);
factory PostgresSchema() => new PostgresSchema._(new StringBuffer(), 0);
Future run(PostgreSQLConnection connection) => connection.execute(compile());
String compile() => _buf.toString();
void _writeln(String str) {
for (int i = 0; i < _indent; i++) {
_buf.write(' ');
}
_buf.writeln(str);
}
@override
void drop(String tableName, {bool cascade: false}) {
var c = cascade == true ? ' CASCADE' : '';
_writeln('DROP TABLE "$tableName"$c;');
}
@override
void alter(String tableName, void callback(MutableTable table)) {
var tbl = new PostgresAlterTable(tableName);
callback(tbl);
_writeln('ALTER TABLE "$tableName"');
tbl.compile(_buf, _indent + 1);
_buf.write(';');
}
void _create(String tableName, void callback(Table table), bool ifNotExists) {
var op = ifNotExists ? ' IF NOT EXISTS' : '';
var tbl = new PostgresTable();
callback(tbl);
_writeln('CREATE TABLE$op "$tableName" (');
tbl.compile(_buf, _indent + 1);
_buf.writeln();
_writeln(');');
}
@override
void create(String tableName, void callback(Table table)) =>
_create(tableName, callback, false);
@override
void createIfNotExists(String tableName, void callback(Table table)) =>
_create(tableName, callback, true);
}

View file

@ -0,0 +1,166 @@
import 'dart:collection';
import 'package:angel_orm/angel_orm.dart';
import 'package:angel_migration/angel_migration.dart';
import 'package:charcode/ascii.dart';
abstract class PostgresGenerator {
static String columnType(MigrationColumn column) {
var str = column.type.name;
if (column.length != null)
return '$str(${column.length})';
else
return str;
}
static String compileColumn(MigrationColumn column) {
var buf = new StringBuffer(columnType(column));
if (column.isNullable == false) buf.write(' NOT NULL');
if (column.defaultValue != null) {
String s;
var value = column.defaultValue;
if (value is RawSql)
s = value.value;
else if (value is String) {
var b = StringBuffer();
for (var ch in value.codeUnits) {
if (ch == $single_quote) {
b.write("\\'");
} else {
b.writeCharCode(ch);
}
}
s = b.toString();
} else {
s = value.toString();
}
buf.write(' DEFAULT $s');
}
if (column.indexType == IndexType.unique)
buf.write(' UNIQUE');
else if (column.indexType == IndexType.primaryKey)
buf.write(' PRIMARY KEY');
for (var ref in column.externalReferences) {
buf.write(' ' + compileReference(ref));
}
return buf.toString();
}
static String compileReference(MigrationColumnReference ref) {
var buf = new StringBuffer(
'REFERENCES "${ref.foreignTable}"("${ref.foreignKey}")');
if (ref.behavior != null) buf.write(' ' + ref.behavior);
return buf.toString();
}
}
class PostgresTable extends Table {
final Map<String, MigrationColumn> _columns = {};
@override
MigrationColumn declareColumn(String name, Column column) {
if (_columns.containsKey(name))
throw new StateError('Cannot redeclare column "$name".');
var col = new MigrationColumn.from(column);
_columns[name] = col;
return col;
}
void compile(StringBuffer buf, int indent) {
int i = 0;
_columns.forEach((name, column) {
var col = PostgresGenerator.compileColumn(column);
if (i++ > 0) buf.writeln(',');
for (int i = 0; i < indent; i++) {
buf.write(' ');
}
buf.write('"$name" $col');
});
}
}
class PostgresAlterTable extends Table implements MutableTable {
final Map<String, MigrationColumn> _columns = {};
final String tableName;
final Queue<String> _stack = new Queue<String>();
PostgresAlterTable(this.tableName);
void compile(StringBuffer buf, int indent) {
int i = 0;
while (_stack.isNotEmpty) {
var str = _stack.removeFirst();
if (i++ > 0) buf.writeln(',');
for (int i = 0; i < indent; i++) {
buf.write(' ');
}
buf.write(str);
}
if (i > 0) buf.writeln(';');
i = 0;
_columns.forEach((name, column) {
var col = PostgresGenerator.compileColumn(column);
if (i++ > 0) buf.writeln(',');
for (int i = 0; i < indent; i++) {
buf.write(' ');
}
buf.write('ADD COLUMN "$name" $col');
});
}
@override
MigrationColumn declareColumn(String name, Column column) {
if (_columns.containsKey(name))
throw new StateError('Cannot redeclare column "$name".');
var col = new MigrationColumn.from(column);
_columns[name] = col;
return col;
}
@override
void dropNotNull(String name) {
_stack.add('ALTER COLUMN "$name" DROP NOT NULL');
}
@override
void setNotNull(String name) {
_stack.add('ALTER COLUMN "$name" SET NOT NULL');
}
@override
void changeColumnType(String name, ColumnType type, {int length}) {
_stack.add('ALTER COLUMN "$name" TYPE ' +
PostgresGenerator.columnType(
new MigrationColumn(type, length: length)));
}
@override
void renameColumn(String name, String newName) {
_stack.add('RENAME COLUMN "$name" TO "$newName"');
}
@override
void dropColumn(String name) {
_stack.add('DROP COLUMN "$name"');
}
@override
void rename(String newName) {
_stack.add('RENAME TO "$newName"');
}
}

View file

@ -0,0 +1,14 @@
import 'dart:async';
import 'package:angel_migration/angel_migration.dart';
abstract class MigrationRunner {
void addMigration(Migration migration);
Future up();
Future rollback();
Future reset();
Future close();
}

View file

@ -0,0 +1,14 @@
import 'dart:async';
import 'dart:isolate';
import 'dart:mirrors';
Future<String> absoluteSourcePath(Type type) async {
var mirror = reflectType(type);
var uri = mirror.location.sourceUri;
if (uri.scheme == 'package') {
uri = await Isolate.resolvePackageUri(uri);
}
return uri.toFilePath() + '#' + MirrorSystem.getName(mirror.simpleName);
}

View file

@ -0,0 +1,13 @@
name: angel_migration_runner
version: 2.0.0
description: Command-line based database migration runner for Angel's ORM.
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/migration
environment:
sdk: ">=2.0.0-dev <3.0.0"
dependencies:
angel_migration: ^2.0.0-alpha
angel_orm: ^2.0.0-dev.2
args: ^1.0.0
charcode: ^1.0.0
postgres: ">=0.9.5 <2.0.0"

58
packages/orm/angel_orm/.gitignore vendored Normal file
View file

@ -0,0 +1,58 @@
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
.packages
.pub/
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
.dart_tool

View file

@ -0,0 +1,146 @@
# 2.1.0-beta.3
* Remove parentheses from `AS` when renaming raw `expressions`.
# 2.1.0-beta.2
* Add `expressions` to `Query`, to support custom SQL expressions that are
read as normal fields.
# 2.1.0-beta.1
* Calls to `leftJoin`, etc. alias all fields in a child query, to prevent
`ambiguous column a0.id` errors.
# 2.1.0-beta
* Split the formerly 600+ line `src/query.dart` up into
separate files.
* **BREAKING**: Add a required `QueryExecutor` argument to `transaction`
callbacks.
* Make `JoinBuilder` take `to` as a `String Function()`. This will allow
ORM queries to reference their joined subqueries.
* Removed deprecated `Join`, `toSql`, `sanitizeExpression`, `isAscii`.
* Always put `ORDER BY` before `LIMIT`.
* `and`, `or`, `not` in `QueryWhere` include parentheses.
* Add `joinType` to `Relationship` class.
# 2.0.2
* Place `LIMIT` and `OFFSET` after `ORDER BY`.
# 2.0.1
* Apply `package:pedantic` fixes.
* `@PrimaryKey()` no longer defaults to `serial`, allowing its type to be
inferenced.
# 2.0.0
* Add `isNull`, `isNotNull` getters to builders.
# 2.0.0-dev.24
* Fix a bug that caused syntax errors on `ORDER BY`.
* Add `pattern` to `like` on string builder. `sanitize` is optional.
* Add `RawSql`.
# 2.0.0-dev.23
* Add `@ManyToMany` annotation, which builds many-to-many relations.
# 2.0.0-dev.22
* `compileInsert` will explicitly never emit a key not belonging to the
associated query.
# 2.0.0-dev.21
* Add tableName to query
# 2.0.0-dev.20
* Join updates.
# 2.0.0-dev.19
* Implement cast-based `double` support.
* Finish `ListSqlExpressionBuilder`.
# 2.0.0-dev.18
* Add `ListSqlExpressionBuilder` (still in development).
# 2.0.0-dev.17
* Add `EnumSqlExpressionBuilder`.
# 2.0.0-dev.16
* Add `MapSqlExpressionBuilder` for JSON/JSONB support.
# 2.0.0-dev.15
* Remove `Column.defaultValue`.
* Deprecate `toSql` and `sanitizeExpression`.
* Refactor builders so that strings are passed through
# 2.0.0-dev.14
* Remove obsolete `@belongsToMany`.
# 2.0.0-dev.13
* Push for consistency with orm_gen @ `2.0.0-dev`.
# 2.0.0-dev.12
* Always apply `toSql` escapes.
# 2.0.0-dev.11
* Remove `limit(1)` except on `getOne`
# 2.0.0-dev.10
* Add `withFields` to `compile()`
# 2.0.0-dev.9
* Permanent preamble fix
# 2.0.0-dev.8
* Escapes
# 2.0.0-dev.7
* Update `toSql`
* Add `isTrue` and `isFalse`
# 2.0.0-dev.6
* Add `delete`, `insert` and `update` methods to `Query`.
# 2.0.0-dev.4
* Add more querying methods.
* Add preamble to `Query.compile`.
# 2.0.0-dev.3
* Brought back old-style query builder.
* Strong-mode updates, revised `Join`.
# 2.0.0-dev.2
* Renamed `ORM` to `Orm`.
* `Orm` now requires a database type.
# 2.0.0-dev.1
* Restored all old PostgreSQL-specific annotations. Rather than a smart runtime,
having a codegen capable of building ORM's for multiple databases can potentially
provide a very fast ORM for everyone.
# 2.0.0-dev
* Removed PostgreSQL-specific functionality, so that the ORM can ultimately
target all services.
* Created a better `Join` model.
* Created a far better `Query` model.
* Removed `lib/server.dart`
# 1.0.0-alpha+10
* Split into `angel_orm.dart` and `server.dart`. Prevents DDC failures.
# 1.0.0-alpha+7
* Added a `@belongsToMany` annotation class.
* Resolved [#20](https://github.com/angel-dart/orm/issues/20). The
`PostgreSQLConnectionPool` keeps track of which connections have been opened now.
# 1.0.0-alpha+6
* `DateTimeSqlExpressionBuilder` will no longer automatically
insert quotation marks around names.
# 1.0.0-alpha+5
* Corrected a typo that was causing the aforementioned test failures.
`==` becomes `=`.
# 1.0.0-alpha+4
* Added a null-check in `lib/src/query.dart#L24` to (hopefully) prevent it from
crashing on Travis.
# 1.0.0-alpha+3
* Added `isIn`, `isNotIn`, `isBetween`, `isNotBetween` to `SqlExpressionBuilder` and its
subclasses.
* Added a dependency on `package:meta`.

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 The Angel Framework
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,6 @@
# angel_orm
Runtime support for Angel's ORM. Includes a clean, database-agnostic
query builder and relationship/join support.
For documentation about the ORM, head to the main project repo:
https://github.com/angel-dart/orm

View file

@ -0,0 +1,4 @@
include: package:pedantic/analysis_options.yaml
analyzer:
strong-mode:
implicit-casts: false

View file

@ -0,0 +1,67 @@
// **************************************************************************
// JsonModelGenerator
// **************************************************************************
@generatedSerializable
class Employee extends _Employee {
Employee(
{this.id,
this.firstName,
this.lastName,
this.salary,
this.createdAt,
this.updatedAt});
@override
final String id;
@override
final String firstName;
@override
final String lastName;
@override
final double salary;
@override
final DateTime createdAt;
@override
final DateTime updatedAt;
Employee copyWith(
{String id,
String firstName,
String lastName,
double salary,
DateTime createdAt,
DateTime updatedAt}) {
return new Employee(
id: id ?? this.id,
firstName: firstName ?? this.firstName,
lastName: lastName ?? this.lastName,
salary: salary ?? this.salary,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt);
}
bool operator ==(other) {
return other is _Employee &&
other.id == id &&
other.firstName == firstName &&
other.lastName == lastName &&
other.salary == salary &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt;
}
@override
int get hashCode {
return hashObjects([id, firstName, lastName, salary, createdAt, updatedAt]);
}
Map<String, dynamic> toJson() {
return EmployeeSerializer.toMap(this);
}
}

View file

@ -0,0 +1,112 @@
import 'dart:async';
import 'package:angel_model/angel_model.dart';
import 'package:angel_orm/angel_orm.dart';
import 'package:angel_orm/src/query.dart';
import 'package:angel_serialize/angel_serialize.dart';
part 'main.g.dart';
part 'main.serializer.g.dart';
main() async {
var query = EmployeeQuery()
..where.firstName.equals('Rich')
..where.lastName.equals('Person')
..orWhere((w) => w.salary.greaterThanOrEqualTo(75000))
..join('companies', 'company_id', 'id');
var richPerson = await query.getOne(_FakeExecutor());
print(richPerson.toJson());
}
class _FakeExecutor extends QueryExecutor {
const _FakeExecutor();
@override
Future<List<List>> query(
String tableName, String query, Map<String, dynamic> substitutionValues,
[returningFields]) async {
var now = DateTime.now();
print(
'_FakeExecutor received query: $query and values: $substitutionValues');
return [
[1, 'Rich', 'Person', 100000.0, now, now]
];
}
@override
Future<T> transaction<T>(FutureOr<T> Function(QueryExecutor) f) {
throw UnsupportedError('Transactions are not supported.');
}
}
@orm
@serializable
abstract class _Employee extends Model {
String get firstName;
String get lastName;
double get salary;
}
class EmployeeQuery extends Query<Employee, EmployeeQueryWhere> {
@override
final QueryValues values = MapQueryValues();
EmployeeQueryWhere _where;
EmployeeQuery() {
_where = EmployeeQueryWhere(this);
}
@override
EmployeeQueryWhere get where => _where;
@override
String get tableName => 'employees';
@override
List<String> get fields =>
['id', 'first_name', 'last_name', 'salary', 'created_at', 'updated_at'];
@override
EmployeeQueryWhere newWhereClause() => EmployeeQueryWhere(this);
@override
Employee deserialize(List row) {
return Employee(
id: row[0].toString(),
firstName: row[1] as String,
lastName: row[2] as String,
salary: row[3] as double,
createdAt: row[4] as DateTime,
updatedAt: row[5] as DateTime);
}
}
class EmployeeQueryWhere extends QueryWhere {
EmployeeQueryWhere(EmployeeQuery query)
: id = NumericSqlExpressionBuilder(query, 'id'),
firstName = StringSqlExpressionBuilder(query, 'first_name'),
lastName = StringSqlExpressionBuilder(query, 'last_name'),
salary = NumericSqlExpressionBuilder(query, 'salary'),
createdAt = DateTimeSqlExpressionBuilder(query, 'created_at'),
updatedAt = DateTimeSqlExpressionBuilder(query, 'updated_at');
@override
Iterable<SqlExpressionBuilder> get expressionBuilders {
return [id, firstName, lastName, salary, createdAt, updatedAt];
}
final NumericSqlExpressionBuilder<int> id;
final StringSqlExpressionBuilder firstName;
final StringSqlExpressionBuilder lastName;
final NumericSqlExpressionBuilder<double> salary;
final DateTimeSqlExpressionBuilder createdAt;
final DateTimeSqlExpressionBuilder updatedAt;
}

View file

@ -0,0 +1,71 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'main.dart';
// **************************************************************************
// JsonModelGenerator
// **************************************************************************
@generatedSerializable
class Employee extends _Employee {
Employee(
{this.id,
this.firstName,
this.lastName,
this.salary,
this.createdAt,
this.updatedAt});
@override
final String id;
@override
final String firstName;
@override
final String lastName;
@override
final double salary;
@override
final DateTime createdAt;
@override
final DateTime updatedAt;
Employee copyWith(
{String id,
String firstName,
String lastName,
double salary,
DateTime createdAt,
DateTime updatedAt}) {
return Employee(
id: id ?? this.id,
firstName: firstName ?? this.firstName,
lastName: lastName ?? this.lastName,
salary: salary ?? this.salary,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt);
}
bool operator ==(other) {
return other is _Employee &&
other.id == id &&
other.firstName == firstName &&
other.lastName == lastName &&
other.salary == salary &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt;
}
@override
int get hashCode {
return hashObjects([id, firstName, lastName, salary, createdAt, updatedAt]);
}
Map<String, dynamic> toJson() {
return EmployeeSerializer.toMap(this);
}
}

View file

@ -0,0 +1,64 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'main.dart';
// **************************************************************************
// SerializerGenerator
// **************************************************************************
abstract class EmployeeSerializer {
static Employee fromMap(Map map) {
return Employee(
id: map['id'] as String,
firstName: map['first_name'] as String,
lastName: map['last_name'] as String,
salary: map['salary'] as double,
createdAt: map['created_at'] != null
? (map['created_at'] is DateTime
? (map['created_at'] as DateTime)
: DateTime.parse(map['created_at'].toString()))
: null,
updatedAt: map['updated_at'] != null
? (map['updated_at'] is DateTime
? (map['updated_at'] as DateTime)
: DateTime.parse(map['updated_at'].toString()))
: null);
}
static Map<String, dynamic> toMap(Employee model) {
if (model == null) {
return null;
}
return {
'id': model.id,
'first_name': model.firstName,
'last_name': model.lastName,
'salary': model.salary,
'created_at': model.createdAt?.toIso8601String(),
'updated_at': model.updatedAt?.toIso8601String()
};
}
}
abstract class EmployeeFields {
static const List<String> allFields = <String>[
id,
firstName,
lastName,
salary,
createdAt,
updatedAt
];
static const String id = 'id';
static const String firstName = 'first_name';
static const String lastName = 'last_name';
static const String salary = 'salary';
static const String createdAt = 'created_at';
static const String updatedAt = 'updated_at';
}

View file

@ -0,0 +1,15 @@
export 'src/annotations.dart';
export 'src/builder.dart';
export 'src/join_builder.dart';
export 'src/join_on.dart';
export 'src/map_query_values.dart';
export 'src/migration.dart';
export 'src/order_by.dart';
export 'src/query_base.dart';
export 'src/query_executor.dart';
export 'src/query_values.dart';
export 'src/query_where.dart';
export 'src/query.dart';
export 'src/relations.dart';
export 'src/union.dart';
export 'src/util.dart';

View file

@ -0,0 +1,31 @@
/// A raw SQL statement that specifies a date/time default to the
/// current time.
const RawSql currentTimestamp = RawSql('CURRENT_TIMESTAMP');
/// Can passed to a [MigrationColumn] to default to a raw SQL expression.
class RawSql {
/// The raw SQL text.
final String value;
const RawSql(this.value);
}
/// Canonical instance of [ORM]. Implies all defaults.
const Orm orm = Orm();
class Orm {
/// The name of the table to query.
///
/// Inferred if not present.
final String tableName;
/// Whether to generate migrations for this model.
///
/// Defaults to [:true:].
final bool generateMigrations;
const Orm({this.tableName, this.generateMigrations = true});
}
/// The various types of join.
enum JoinType { inner, left, right, full, self }

View file

@ -0,0 +1,651 @@
import 'dart:convert';
import 'package:intl/intl.dart' show DateFormat;
import 'query.dart';
final DateFormat dateYmd = DateFormat('yyyy-MM-dd');
final DateFormat dateYmdHms = DateFormat('yyyy-MM-dd HH:mm:ss');
abstract class SqlExpressionBuilder<T> {
final Query query;
final String columnName;
String _cast;
bool _isProperty = false;
String _substitution;
SqlExpressionBuilder(this.query, this.columnName);
String get substitution {
var c = _isProperty ? 'prop' : columnName;
return _substitution ??= query.reserveName(c);
}
bool get hasValue;
String compile();
}
class NumericSqlExpressionBuilder<T extends num>
extends SqlExpressionBuilder<T> {
bool _hasValue = false;
String _op = '=';
String _raw;
T _value;
NumericSqlExpressionBuilder(Query query, String columnName)
: super(query, columnName);
@override
bool get hasValue => _hasValue;
bool _change(String op, T value) {
_raw = null;
_op = op;
_value = value;
return _hasValue = true;
}
@override
String compile() {
if (_raw != null) return _raw;
if (_value == null) return null;
var v = _value.toString();
if (T == double) v = 'CAST ("$v" as decimal)';
if (_cast != null) v = 'CAST ($v AS $_cast)';
return '$_op $v';
}
operator <(T value) => _change('<', value);
operator >(T value) => _change('>', value);
operator <=(T value) => _change('<=', value);
operator >=(T value) => _change('>=', value);
void get isNull {
_raw = 'IS NULL';
_hasValue = true;
}
void get isNotNull {
_raw = 'IS NOT NULL';
_hasValue = true;
}
void lessThan(T value) {
_change('<', value);
}
void lessThanOrEqualTo(T value) {
_change('<=', value);
}
void greaterThan(T value) {
_change('>', value);
}
void greaterThanOrEqualTo(T value) {
_change('>=', value);
}
void equals(T value) {
_change('=', value);
}
void notEquals(T value) {
_change('!=', value);
}
void isBetween(T lower, T upper) {
_raw = 'BETWEEN $lower AND $upper';
_hasValue = true;
}
void isNotBetween(T lower, T upper) {
_raw = 'NOT BETWEEN $lower AND $upper';
_hasValue = true;
}
void isIn(Iterable<T> values) {
_raw = 'IN (' + values.join(', ') + ')';
_hasValue = true;
}
void isNotIn(Iterable<T> values) {
_raw = 'NOT IN (' + values.join(', ') + ')';
_hasValue = true;
}
}
class EnumSqlExpressionBuilder<T> extends SqlExpressionBuilder<T> {
final int Function(T) _getValue;
bool _hasValue = false;
String _op = '=';
String _raw;
int _value;
EnumSqlExpressionBuilder(Query query, String columnName, this._getValue)
: super(query, columnName);
@override
bool get hasValue => _hasValue;
bool _change(String op, T value) {
_raw = null;
_op = op;
_value = _getValue(value);
return _hasValue = true;
}
UnsupportedError _unsupported() =>
UnsupportedError('Enums do not support this operation.');
@override
String compile() {
if (_raw != null) return _raw;
if (_value == null) return null;
return '$_op $_value';
}
void get isNull {
_raw = 'IS NULL';
_hasValue = true;
}
void get isNotNull {
_raw = 'IS NOT NULL';
_hasValue = true;
}
void equals(T value) {
_change('=', value);
}
void notEquals(T value) {
_change('!=', value);
}
void isBetween(T lower, T upper) => throw _unsupported();
void isNotBetween(T lower, T upper) => throw _unsupported();
void isIn(Iterable<T> values) {
_raw = 'IN (' + values.map(_getValue).join(', ') + ')';
_hasValue = true;
}
void isNotIn(Iterable<T> values) {
_raw = 'NOT IN (' + values.map(_getValue).join(', ') + ')';
_hasValue = true;
}
}
class StringSqlExpressionBuilder extends SqlExpressionBuilder<String> {
bool _hasValue = false;
String _op = '=', _raw, _value;
StringSqlExpressionBuilder(Query query, String columnName)
: super(query, columnName);
@override
bool get hasValue => _hasValue;
String get lowerName => '${substitution}_lower';
String get upperName => '${substitution}_upper';
bool _change(String op, String value) {
_raw = null;
_op = op;
_value = value;
query.substitutionValues[substitution] = _value;
return _hasValue = true;
}
@override
String compile() {
if (_raw != null) return _raw;
if (_value == null) return null;
return "$_op @$substitution";
}
void isEmpty() => equals('');
void equals(String value) {
_change('=', value);
}
void notEquals(String value) {
_change('!=', value);
}
/// Builds a `LIKE` predicate.
///
/// To prevent injections, an optional [sanitizer] is called with a name that
/// will be escaped by the underlying [QueryExecutor]. Use this if the [pattern]
/// is not constant, and/or involves user input.
///
/// Otherwise, you can omit [sanitizer].
///
/// Example:
/// ```dart
/// carNameBuilder.like('%Mazda%');
/// carNameBuilder.like((name) => 'Mazda %$name%');
/// ```
void like(String pattern, {String Function(String) sanitize}) {
sanitize ??= (s) => pattern;
_raw = 'LIKE \'' + sanitize('@$substitution') + '\'';
query.substitutionValues[substitution] = pattern;
_hasValue = true;
_value = null;
}
void isBetween(String lower, String upper) {
query.substitutionValues[lowerName] = lower;
query.substitutionValues[upperName] = upper;
_raw = "BETWEEN @$lowerName AND @$upperName";
_hasValue = true;
}
void isNotBetween(String lower, String upper) {
query.substitutionValues[lowerName] = lower;
query.substitutionValues[upperName] = upper;
_raw = "NOT BETWEEN @$lowerName AND @$upperName";
_hasValue = true;
}
void get isNull {
_raw = 'IS NULL';
_hasValue = true;
}
void get isNotNull {
_raw = 'IS NOT NULL';
_hasValue = true;
}
String _in(Iterable<String> values) {
return 'IN (' +
values.map((v) {
var name = query.reserveName('${columnName}_in_value');
query.substitutionValues[name] = v;
return '@$name';
}).join(', ') +
')';
}
void isIn(Iterable<String> values) {
_raw = _in(values);
_hasValue = true;
}
void isNotIn(Iterable<String> values) {
_raw = 'NOT ' + _in(values);
_hasValue = true;
}
}
class BooleanSqlExpressionBuilder extends SqlExpressionBuilder<bool> {
bool _hasValue = false;
String _op = '=', _raw;
bool _value;
BooleanSqlExpressionBuilder(Query query, String columnName)
: super(query, columnName);
@override
bool get hasValue => _hasValue;
bool _change(String op, bool value) {
_raw = null;
_op = op;
_value = value;
return _hasValue = true;
}
@override
String compile() {
if (_raw != null) return _raw;
if (_value == null) return null;
var v = _value ? 'TRUE' : 'FALSE';
if (_cast != null) v = 'CAST ($v AS $_cast)';
return '$_op $v';
}
void get isTrue => equals(true);
void get isFalse => equals(false);
void get isNull {
_raw = 'IS NULL';
_hasValue = true;
}
void get isNotNull {
_raw = 'IS NOT NULL';
_hasValue = true;
}
void equals(bool value) {
_change('=', value);
}
void notEquals(bool value) {
_change('!=', value);
}
}
class DateTimeSqlExpressionBuilder extends SqlExpressionBuilder<DateTime> {
NumericSqlExpressionBuilder<int> _year, _month, _day, _hour, _minute, _second;
String _raw;
DateTimeSqlExpressionBuilder(Query query, String columnName)
: super(query, columnName);
NumericSqlExpressionBuilder<int> get year =>
_year ??= NumericSqlExpressionBuilder(query, 'year');
NumericSqlExpressionBuilder<int> get month =>
_month ??= NumericSqlExpressionBuilder(query, 'month');
NumericSqlExpressionBuilder<int> get day =>
_day ??= NumericSqlExpressionBuilder(query, 'day');
NumericSqlExpressionBuilder<int> get hour =>
_hour ??= NumericSqlExpressionBuilder(query, 'hour');
NumericSqlExpressionBuilder<int> get minute =>
_minute ??= NumericSqlExpressionBuilder(query, 'minute');
NumericSqlExpressionBuilder<int> get second =>
_second ??= NumericSqlExpressionBuilder(query, 'second');
@override
bool get hasValue =>
_raw?.isNotEmpty == true ||
_year?.hasValue == true ||
_month?.hasValue == true ||
_day?.hasValue == true ||
_hour?.hasValue == true ||
_minute?.hasValue == true ||
_second?.hasValue == true;
bool _change(String _op, DateTime dt, bool time) {
var dateString = time ? dateYmdHms.format(dt) : dateYmd.format(dt);
_raw = '$columnName $_op \'$dateString\'';
return true;
}
operator <(DateTime value) => _change('<', value, true);
operator <=(DateTime value) => _change('<=', value, true);
operator >(DateTime value) => _change('>', value, true);
operator >=(DateTime value) => _change('>=', value, true);
void equals(DateTime value, {bool includeTime = true}) {
_change('=', value, includeTime != false);
}
void lessThan(DateTime value, {bool includeTime = true}) {
_change('<', value, includeTime != false);
}
void lessThanOrEqualTo(DateTime value, {bool includeTime = true}) {
_change('<=', value, includeTime != false);
}
void greaterThan(DateTime value, {bool includeTime = true}) {
_change('>', value, includeTime != false);
}
void greaterThanOrEqualTo(DateTime value, {bool includeTime = true}) {
_change('>=', value, includeTime != false);
}
void isIn(Iterable<DateTime> values) {
_raw = '$columnName IN (' +
values.map(dateYmdHms.format).map((s) => '$s').join(', ') +
')';
}
void isNotIn(Iterable<DateTime> values) {
_raw = '$columnName NOT IN (' +
values.map(dateYmdHms.format).map((s) => '$s').join(', ') +
')';
}
void isBetween(DateTime lower, DateTime upper) {
var l = dateYmdHms.format(lower), u = dateYmdHms.format(upper);
_raw = "$columnName BETWEEN '$l' and '$u'";
}
void isNotBetween(DateTime lower, DateTime upper) {
var l = dateYmdHms.format(lower), u = dateYmdHms.format(upper);
_raw = "$columnName NOT BETWEEN '$l' and '$u'";
}
void get isNull {
_raw = '$columnName IS NULL';
}
void get isNotNull {
_raw = '$columnName IS NOT NULL';
}
@override
String compile() {
if (_raw?.isNotEmpty == true) return _raw;
List<String> parts = [];
if (year?.hasValue == true) {
parts.add('YEAR($columnName) ${year.compile()}');
}
if (month?.hasValue == true) {
parts.add('MONTH($columnName) ${month.compile()}');
}
if (day?.hasValue == true) {
parts.add('DAY($columnName) ${day.compile()}');
}
if (hour?.hasValue == true) {
parts.add('HOUR($columnName) ${hour.compile()}');
}
if (minute?.hasValue == true) {
parts.add('MINUTE($columnName) ${minute.compile()}');
}
if (second?.hasValue == true) {
parts.add('SECOND($columnName) ${second.compile()}');
}
return parts.isEmpty ? null : parts.join(' AND ');
}
}
abstract class JsonSqlExpressionBuilder<T, K> extends SqlExpressionBuilder<T> {
final List<JsonSqlExpressionBuilderProperty> _properties = [];
bool _hasValue = false;
T _value;
String _op;
String _raw;
JsonSqlExpressionBuilder(Query query, String columnName)
: super(query, columnName);
JsonSqlExpressionBuilderProperty operator [](K name) {
var p = _property(name);
_properties.add(p);
return p;
}
JsonSqlExpressionBuilderProperty _property(K name);
bool get hasRaw => _raw != null || _properties.any((p) => p.hasValue);
@override
bool get hasValue => _hasValue || _properties.any((p) => p.hasValue);
_encodeValue(T v) => v;
bool _change(String op, T value) {
_raw = null;
_op = op;
_value = value;
query.substitutionValues[substitution] = _encodeValue(_value);
return _hasValue = true;
}
void get isNull {
_raw = 'IS NULL';
_hasValue = true;
}
void get isNotNull {
_raw = 'IS NOT NULL';
_hasValue = true;
}
@override
String compile() {
var s = _compile();
if (!_properties.any((p) => p.hasValue)) return s;
s ??= '';
for (var p in _properties) {
if (p.hasValue) {
var c = p.compile();
if (c != null) {
_hasValue = true;
s ??= '';
if (p.typed is! DateTimeSqlExpressionBuilder) {
s += '${p.typed.columnName} ';
}
s += c;
}
}
}
return s;
}
String _compile() {
if (_raw != null) return _raw;
if (_value == null) return null;
return "::jsonb $_op @$substitution::jsonb";
}
void contains(T value) {
_change('@>', value);
}
void equals(T value) {
_change('=', value);
}
}
class MapSqlExpressionBuilder extends JsonSqlExpressionBuilder<Map, String> {
MapSqlExpressionBuilder(Query query, String columnName)
: super(query, columnName);
@override
JsonSqlExpressionBuilderProperty _property(String name) {
return JsonSqlExpressionBuilderProperty(this, name, false);
}
void containsKey(String key) {
this[key].isNotNull;
}
void containsPair(key, value) {
contains({key: value});
}
}
class ListSqlExpressionBuilder extends JsonSqlExpressionBuilder<List, int> {
ListSqlExpressionBuilder(Query query, String columnName)
: super(query, columnName);
@override
_encodeValue(List v) => json.encode(v);
@override
JsonSqlExpressionBuilderProperty _property(int name) {
return JsonSqlExpressionBuilderProperty(this, name.toString(), true);
}
}
class JsonSqlExpressionBuilderProperty {
final JsonSqlExpressionBuilder builder;
final String name;
final bool isInt;
SqlExpressionBuilder _typed;
JsonSqlExpressionBuilderProperty(this.builder, this.name, this.isInt);
SqlExpressionBuilder get typed => _typed;
bool get hasValue => _typed?.hasValue == true;
String compile() => _typed?.compile();
T _set<T extends SqlExpressionBuilder>(T Function() value) {
if (_typed is T) {
return _typed as T;
} else if (_typed != null) {
throw StateError(
'$nameString is already typed as $_typed, and cannot be changed.');
} else {
_typed = value()
.._cast = 'text'
.._isProperty = true;
return _typed as T;
}
}
String get nameString {
var n = isInt ? name : "'$name'";
return '${builder.columnName}::jsonb->>$n';
}
void get isNotNull {
builder
.._hasValue = true
.._raw ??= ''
.._raw += "$nameString IS NOT NULL";
}
void get isNull {
builder
.._hasValue = true
.._raw ??= ''
.._raw += "$nameString IS NULL";
}
StringSqlExpressionBuilder get asString {
return _set(() => StringSqlExpressionBuilder(builder.query, nameString));
}
BooleanSqlExpressionBuilder get asBool {
return _set(() => BooleanSqlExpressionBuilder(builder.query, nameString));
}
DateTimeSqlExpressionBuilder get asDateTime {
return _set(() => DateTimeSqlExpressionBuilder(builder.query, nameString));
}
NumericSqlExpressionBuilder<double> get asDouble {
return _set(
() => NumericSqlExpressionBuilder<double>(builder.query, nameString));
}
NumericSqlExpressionBuilder<int> get asInt {
return _set(
() => NumericSqlExpressionBuilder<int>(builder.query, nameString));
}
MapSqlExpressionBuilder get asMap {
return _set(() => MapSqlExpressionBuilder(builder.query, nameString));
}
ListSqlExpressionBuilder get asList {
return _set(() => ListSqlExpressionBuilder(builder.query, nameString));
}
}

View file

@ -0,0 +1,71 @@
import 'annotations.dart';
import 'query.dart';
/// Builds a SQL `JOIN` query.
class JoinBuilder {
final JoinType type;
final Query from;
final String key, value, op, alias;
final bool aliasAllFields;
/// A callback to produces the expression to join against, i.e.
/// a table name, or the result of compiling a query.
final String Function() to;
final List<String> additionalFields;
JoinBuilder(this.type, this.from, this.to, this.key, this.value,
{this.op = '=',
this.alias,
this.additionalFields = const [],
this.aliasAllFields = false}) {
assert(to != null,
'computation of this join threw an error, and returned null.');
}
String get fieldName {
var v = value;
if (aliasAllFields) {
v = '${alias}_$v';
}
var right = '${from.tableName}.$v';
if (alias != null) right = '$alias.$v';
return right;
}
String nameFor(String name) {
if (aliasAllFields) name = '${alias}_$name';
var right = '${from.tableName}.$name';
if (alias != null) right = '$alias.$name';
return right;
}
String compile(Set<String> trampoline) {
var compiledTo = to();
if (compiledTo == null) return null;
var b = StringBuffer();
var left = '${from.tableName}.$key';
var right = fieldName;
switch (type) {
case JoinType.inner:
b.write(' INNER JOIN');
break;
case JoinType.left:
b.write(' LEFT JOIN');
break;
case JoinType.right:
b.write(' RIGHT JOIN');
break;
case JoinType.full:
b.write(' FULL OUTER JOIN');
break;
case JoinType.self:
b.write(' SELF JOIN');
break;
}
b.write(' $compiledTo');
if (alias != null) b.write(' $alias');
b.write(' ON $left$op$right');
return b.toString();
}
}

View file

@ -0,0 +1,8 @@
import 'builder.dart';
class JoinOn {
final SqlExpressionBuilder key;
final SqlExpressionBuilder value;
JoinOn(this.key, this.value);
}

View file

@ -0,0 +1,9 @@
import 'query_values.dart';
/// A [QueryValues] implementation that simply writes to a [Map].
class MapQueryValues extends QueryValues {
final Map<String, dynamic> values = {};
@override
Map<String, dynamic> toMap() => values;
}

View file

@ -0,0 +1,129 @@
const List<String> SQL_RESERVED_WORDS = [
'SELECT',
'UPDATE',
'INSERT',
'DELETE',
'FROM',
'ASC',
'DESC',
'VALUES',
'RETURNING',
'ORDER',
'BY',
];
/// Applies additional attributes to a database column.
class Column {
/// If `true`, a SQL field will be nullable.
final bool isNullable;
/// Specifies the length of a `VARCHAR`.
final int length;
/// Explicitly defines a SQL type for this column.
final ColumnType type;
/// Specifies what kind of index this column is, if any.
final IndexType indexType;
/// A custom SQL expression to execute, instead of a named column.
final String expression;
const Column(
{this.isNullable = true,
this.length,
this.type,
this.indexType = IndexType.none,
this.expression});
/// Returns `true` if [expression] is not `null`.
bool get hasExpression => expression != null;
}
class PrimaryKey extends Column {
const PrimaryKey({ColumnType columnType})
: super(type: columnType, indexType: IndexType.primaryKey);
}
const Column primaryKey = PrimaryKey();
/// Maps to SQL index types.
enum IndexType {
none,
/// Standard index.
standardIndex,
/// A primary key.
primaryKey,
/// A *unique* index.
unique
}
/// Maps to SQL data types.
///
/// Features all types from this list: http://www.tutorialspoint.com/sql/sql-data-types.htm
class ColumnType {
/// The name of this data type.
final String name;
const ColumnType(this.name);
static const ColumnType boolean = ColumnType('boolean');
static const ColumnType smallSerial = ColumnType('smallserial');
static const ColumnType serial = ColumnType('serial');
static const ColumnType bigSerial = ColumnType('bigserial');
// Numbers
static const ColumnType bigInt = ColumnType('bigint');
static const ColumnType int = ColumnType('int');
static const ColumnType smallInt = ColumnType('smallint');
static const ColumnType tinyInt = ColumnType('tinyint');
static const ColumnType bit = ColumnType('bit');
static const ColumnType decimal = ColumnType('decimal');
static const ColumnType numeric = ColumnType('numeric');
static const ColumnType money = ColumnType('money');
static const ColumnType smallMoney = ColumnType('smallmoney');
static const ColumnType float = ColumnType('float');
static const ColumnType real = ColumnType('real');
// Dates and times
static const ColumnType dateTime = ColumnType('datetime');
static const ColumnType smallDateTime = ColumnType('smalldatetime');
static const ColumnType date = ColumnType('date');
static const ColumnType time = ColumnType('time');
static const ColumnType timeStamp = ColumnType('timestamp');
static const ColumnType timeStampWithTimeZone =
ColumnType('timestamp with time zone');
// Strings
static const ColumnType char = ColumnType('char');
static const ColumnType varChar = ColumnType('varchar');
static const ColumnType varCharMax = ColumnType('varchar(max)');
static const ColumnType text = ColumnType('text');
// Unicode strings
static const ColumnType nChar = ColumnType('nchar');
static const ColumnType nVarChar = ColumnType('nvarchar');
static const ColumnType nVarCharMax = ColumnType('nvarchar(max)');
static const ColumnType nText = ColumnType('ntext');
// Binary
static const ColumnType binary = ColumnType('binary');
static const ColumnType varBinary = ColumnType('varbinary');
static const ColumnType varBinaryMax = ColumnType('varbinary(max)');
static const ColumnType image = ColumnType('image');
// JSON.
static const ColumnType json = ColumnType('json');
static const ColumnType jsonb = ColumnType('jsonb');
// Misc.
static const ColumnType sqlVariant = ColumnType('sql_variant');
static const ColumnType uniqueIdentifier = ColumnType('uniqueidentifier');
static const ColumnType xml = ColumnType('xml');
static const ColumnType cursor = ColumnType('cursor');
static const ColumnType table = ColumnType('table');
}

View file

@ -0,0 +1,8 @@
class OrderBy {
final String key;
final bool descending;
const OrderBy(this.key, {this.descending = false});
String compile() => descending ? '$key DESC' : '$key ASC';
}

View file

@ -0,0 +1,382 @@
import 'dart:async';
import 'annotations.dart';
import 'join_builder.dart';
import 'order_by.dart';
import 'query_base.dart';
import 'query_executor.dart';
import 'query_values.dart';
import 'query_where.dart';
/// A SQL `SELECT` query builder.
abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
final List<JoinBuilder> _joins = [];
final Map<String, int> _names = {};
final List<OrderBy> _orderBy = [];
// An optional "parent query". If provided, [reserveName] will operate in
// the parent's context.
final Query parent;
/// A map of field names to explicit SQL expressions. The expressions will be aliased
/// to the given names.
final Map<String, String> expressions = {};
String _crossJoin, _groupBy;
int _limit, _offset;
Query({this.parent});
Map<String, dynamic> get substitutionValues =>
parent?.substitutionValues ?? super.substitutionValues;
/// A reference to an abstract query builder.
///
/// This is usually a generated class.
Where get where;
/// A set of values, for an insertion or update.
///
/// This is usually a generated class.
QueryValues get values;
/// Preprends the [tableName] to the [String], [s].
String adornWithTableName(String s) {
if (expressions.containsKey(s)) {
return '${expressions[s]} AS $s';
// return '(${expressions[s]} AS $s)';
} else {
return '$tableName.$s';
}
}
/// Returns a unique version of [name], which will not produce a collision within
/// the context of this [query].
String reserveName(String name) {
if (parent != null) return parent.reserveName(name);
var n = _names[name] ??= 0;
_names[name]++;
return n == 0 ? name : '${name}$n';
}
/// Makes a [Where] clause.
Where newWhereClause() {
throw UnsupportedError(
'This instance does not support creating WHERE clauses.');
}
/// Determines whether this query can be compiled.
///
/// Used to prevent ambiguities in joins.
bool canCompile(Set<String> trampoline) => true;
/// Shorthand for calling [where].or with a [Where] clause.
void andWhere(void Function(Where) f) {
var w = newWhereClause();
f(w);
where.and(w);
}
/// Shorthand for calling [where].or with a [Where] clause.
void notWhere(void Function(Where) f) {
var w = newWhereClause();
f(w);
where.not(w);
}
/// Shorthand for calling [where].or with a [Where] clause.
void orWhere(void Function(Where) f) {
var w = newWhereClause();
f(w);
where.or(w);
}
/// Limit the number of rows to return.
void limit(int n) {
_limit = n;
}
/// Skip a number of rows in the query.
void offset(int n) {
_offset = n;
}
/// Groups the results by a given key.
void groupBy(String key) {
_groupBy = key;
}
/// Sorts the results by a key.
void orderBy(String key, {bool descending = false}) {
_orderBy.add(OrderBy(key, descending: descending));
}
/// Execute a `CROSS JOIN` (Cartesian product) against another table.
void crossJoin(String tableName) {
_crossJoin = tableName;
}
String _joinAlias(Set<String> trampoline) {
int i = _joins.length;
while (true) {
var a = 'a$i';
if (trampoline.add(a)) {
return a;
} else {
i++;
}
}
}
String Function() _compileJoin(tableName, Set<String> trampoline) {
if (tableName is String) {
return () => tableName;
} else if (tableName is Query) {
return () {
var c = tableName.compile(trampoline);
if (c == null) return c;
return '($c)';
};
} else {
throw ArgumentError.value(
tableName, 'tableName', 'must be a String or Query');
}
}
void _makeJoin(
tableName,
Set<String> trampoline,
JoinType type,
String localKey,
String foreignKey,
String op,
List<String> additionalFields) {
trampoline ??= Set();
// Pivot tables guard against ambiguous fields by excluding tables
// that have already been queried in this scope.
if (trampoline.contains(tableName) && trampoline.contains(this.tableName)) {
// ex. if we have {roles, role_users}, then don't join "roles" again.
return;
}
var to = _compileJoin(tableName, trampoline);
if (to != null) {
var alias = _joinAlias(trampoline);
if (tableName is Query) {
for (var field in tableName.fields) {
tableName.aliases[field] = '${alias}_$field';
}
}
_joins.add(JoinBuilder(type, this, to, localKey, foreignKey,
op: op,
alias: alias,
additionalFields: additionalFields,
aliasAllFields: tableName is Query));
}
}
/// Execute an `INNER JOIN` against another table.
void join(tableName, String localKey, String foreignKey,
{String op = '=',
List<String> additionalFields = const [],
Set<String> trampoline}) {
_makeJoin(tableName, trampoline, JoinType.inner, localKey, foreignKey, op,
additionalFields);
}
/// Execute a `LEFT JOIN` against another table.
void leftJoin(tableName, String localKey, String foreignKey,
{String op = '=',
List<String> additionalFields = const [],
Set<String> trampoline}) {
_makeJoin(tableName, trampoline, JoinType.left, localKey, foreignKey, op,
additionalFields);
}
/// Execute a `RIGHT JOIN` against another table.
void rightJoin(tableName, String localKey, String foreignKey,
{String op = '=',
List<String> additionalFields = const [],
Set<String> trampoline}) {
_makeJoin(tableName, trampoline, JoinType.right, localKey, foreignKey, op,
additionalFields);
}
/// Execute a `FULL OUTER JOIN` against another table.
void fullOuterJoin(tableName, String localKey, String foreignKey,
{String op = '=',
List<String> additionalFields = const [],
Set<String> trampoline}) {
_makeJoin(tableName, trampoline, JoinType.full, localKey, foreignKey, op,
additionalFields);
}
/// Execute a `SELF JOIN`.
void selfJoin(tableName, String localKey, String foreignKey,
{String op = '=',
List<String> additionalFields = const [],
Set<String> trampoline}) {
_makeJoin(tableName, trampoline, JoinType.self, localKey, foreignKey, op,
additionalFields);
}
@override
String compile(Set<String> trampoline,
{bool includeTableName = false,
String preamble,
bool withFields = true,
String fromQuery}) {
// One table MAY appear multiple times in a query.
if (!canCompile(trampoline)) {
return null;
}
includeTableName = includeTableName || _joins.isNotEmpty;
var b = StringBuffer(preamble ?? 'SELECT');
b.write(' ');
List<String> f;
var compiledJoins = <JoinBuilder, String>{};
if (fields == null) {
f = ['*'];
} else {
f = List<String>.from(fields.map((s) {
var ss = includeTableName ? '$tableName.$s' : s;
if (expressions.containsKey(s)) {
// ss = '(' + expressions[s] + ')';
ss = expressions[s];
}
var cast = casts[s];
if (cast != null) ss = 'CAST ($ss AS $cast)';
if (aliases.containsKey(s)) {
if (cast != null) {
ss = '($ss) AS ${aliases[s]}';
} else {
ss = '$ss AS ${aliases[s]}';
}
if (expressions.containsKey(s)) {
// ss = '($ss)';
}
} else if (expressions.containsKey(s)) {
if (cast != null) {
ss = '($ss) AS $s';
// ss = '(($ss) AS $s)';
} else {
ss = '$ss AS $s';
// ss = '($ss AS $s)';
}
}
return ss;
}));
_joins.forEach((j) {
var c = compiledJoins[j] = j.compile(trampoline);
if (c != null) {
var additional = j.additionalFields.map(j.nameFor).toList();
f.addAll(additional);
} else {
// If compilation failed, fill in NULL placeholders.
for (var i = 0; i < j.additionalFields.length; i++) {
f.add('NULL');
}
}
});
}
if (withFields) b.write(f.join(', '));
fromQuery ??= tableName;
b.write(' FROM $fromQuery');
// No joins if it's not a select.
if (preamble == null) {
if (_crossJoin != null) b.write(' CROSS JOIN $_crossJoin');
for (var join in _joins) {
var c = compiledJoins[join];
if (c != null) b.write(' $c');
}
}
var whereClause =
where.compile(tableName: includeTableName ? tableName : null);
if (whereClause.isNotEmpty) b.write(' WHERE $whereClause');
if (_groupBy != null) b.write(' GROUP BY $_groupBy');
for (var item in _orderBy) {
b.write(' ORDER BY ${item.compile()}');
}
if (_limit != null) b.write(' LIMIT $_limit');
if (_offset != null) b.write(' OFFSET $_offset');
return b.toString();
}
@override
Future<T> getOne(QueryExecutor executor) {
//limit(1);
return super.getOne(executor);
}
Future<List<T>> delete(QueryExecutor executor) {
var sql = compile(Set(), preamble: 'DELETE', withFields: false);
if (_joins.isEmpty) {
return executor
.query(tableName, sql, substitutionValues,
fields.map(adornWithTableName).toList())
.then((it) => it.map(deserialize).toList());
} else {
return executor.transaction((tx) async {
// TODO: Can this be done with just *one* query?
var existing = await get(tx);
//var sql = compile(preamble: 'SELECT $tableName.id', withFields: false);
return tx
.query(tableName, sql, substitutionValues)
.then((_) => existing);
});
}
}
Future<T> deleteOne(QueryExecutor executor) {
return delete(executor).then((it) => it.isEmpty ? null : it.first);
}
Future<T> insert(QueryExecutor executor) {
var insertion = values.compileInsert(this, tableName);
if (insertion == null) {
throw StateError('No values have been specified for update.');
} else {
// TODO: How to do this in a non-Postgres DB?
var returning = fields.map(adornWithTableName).join(', ');
var sql = compile(Set());
sql = 'WITH $tableName as ($insertion RETURNING $returning) ' + sql;
return executor
.query(tableName, sql, substitutionValues)
.then((it) => it.isEmpty ? null : deserialize(it.first));
}
}
Future<List<T>> update(QueryExecutor executor) async {
var updateSql = StringBuffer('UPDATE $tableName ');
var valuesClause = values.compileForUpdate(this);
if (valuesClause == null) {
throw StateError('No values have been specified for update.');
} else {
updateSql.write(' $valuesClause');
var whereClause = where.compile();
if (whereClause.isNotEmpty) updateSql.write(' WHERE $whereClause');
if (_limit != null) updateSql.write(' LIMIT $_limit');
var returning = fields.map(adornWithTableName).join(', ');
var sql = compile(Set());
sql = 'WITH $tableName as ($updateSql RETURNING $returning) ' + sql;
return executor
.query(tableName, sql, substitutionValues)
.then((it) => it.map(deserialize).toList());
}
}
Future<T> updateOne(QueryExecutor executor) {
return update(executor).then((it) => it.isEmpty ? null : it.first);
}
}

View file

@ -0,0 +1,58 @@
import 'dart:async';
import 'query_executor.dart';
import 'union.dart';
/// A base class for objects that compile to SQL queries, typically within an ORM.
abstract class QueryBase<T> {
/// Casts to perform when querying the database.
Map<String, String> get casts => {};
/// `AS` aliases to inject into the query, if any.
Map<String, String> aliases = {};
/// Values to insert into a prepared statement.
final Map<String, dynamic> substitutionValues = {};
/// The table against which to execute this query.
String get tableName;
/// The list of fields returned by this query.
///
/// If it's `null`, then this query will perform a `SELECT *`.
List<String> get fields;
/// A String of all [fields], joined by a comma (`,`).
String get fieldSet => fields.map((k) {
var cast = casts[k];
if (!aliases.containsKey(k)) {
return cast == null ? k : 'CAST ($k AS $cast)';
} else {
var inner = cast == null ? k : '(CAST ($k AS $cast))';
return '$inner AS ${aliases[k]}';
}
}).join(', ');
String compile(Set<String> trampoline,
{bool includeTableName = false, String preamble, bool withFields = true});
T deserialize(List row);
Future<List<T>> get(QueryExecutor executor) async {
var sql = compile(Set());
return executor
.query(tableName, sql, substitutionValues)
.then((it) => it.map(deserialize).toList());
}
Future<T> getOne(QueryExecutor executor) {
return get(executor).then((it) => it.isEmpty ? null : it.first);
}
Union<T> union(QueryBase<T> other) {
return Union(this, other);
}
Union<T> unionAll(QueryBase<T> other) {
return Union(this, other, all: true);
}
}

View file

@ -0,0 +1,23 @@
import 'dart:async';
/// An abstract interface that performs queries.
///
/// This class should be implemented.
abstract class QueryExecutor {
const QueryExecutor();
/// Executes a single query.
Future<List<List>> query(
String tableName, String query, Map<String, dynamic> substitutionValues,
[List<String> returningFields]);
/// Enters a database transaction, performing the actions within,
/// and returning the results of [f].
///
/// If [f] fails, the transaction will be rolled back, and the
/// responsible exception will be re-thrown.
///
/// Whether nested transactions are supported depends on the
/// underlying driver.
Future<T> transaction<T>(FutureOr<T> Function(QueryExecutor) f);
}

View file

@ -0,0 +1,59 @@
import 'query.dart';
abstract class QueryValues {
Map<String, String> get casts => {};
Map<String, dynamic> toMap();
String applyCast(String name, String sub) {
if (casts.containsKey(name)) {
var type = casts[name];
return 'CAST ($sub as $type)';
} else {
return sub;
}
}
String compileInsert(Query query, String tableName) {
var data = Map<String, dynamic>.from(toMap());
var keys = data.keys.toList();
keys.where((k) => !query.fields.contains(k)).forEach(data.remove);
if (data.isEmpty) return null;
var fieldSet = data.keys.join(', ');
var b = StringBuffer('INSERT INTO $tableName ($fieldSet) VALUES (');
int i = 0;
for (var entry in data.entries) {
if (i++ > 0) b.write(', ');
var name = query.reserveName(entry.key);
var s = applyCast(entry.key, '@$name');
query.substitutionValues[name] = entry.value;
b.write(s);
}
b.write(')');
return b.toString();
}
String compileForUpdate(Query query) {
var data = toMap();
if (data.isEmpty) return null;
var b = StringBuffer('SET');
int i = 0;
for (var entry in data.entries) {
if (i++ > 0) b.write(',');
b.write(' ');
b.write(entry.key);
b.write('=');
var name = query.reserveName(entry.key);
var s = applyCast(entry.key, '@$name');
query.substitutionValues[name] = entry.value;
b.write(s);
}
return b.toString();
}
}

View file

@ -0,0 +1,59 @@
import 'builder.dart';
/// Builds a SQL `WHERE` clause.
abstract class QueryWhere {
final Set<QueryWhere> _and = Set();
final Set<QueryWhere> _not = Set();
final Set<QueryWhere> _or = Set();
Iterable<SqlExpressionBuilder> get expressionBuilders;
void and(QueryWhere other) {
_and.add(other);
}
void not(QueryWhere other) {
_not.add(other);
}
void or(QueryWhere other) {
_or.add(other);
}
String compile({String tableName}) {
var b = StringBuffer();
int i = 0;
for (var builder in expressionBuilders) {
var key = builder.columnName;
if (tableName != null) key = '$tableName.$key';
if (builder.hasValue) {
if (i++ > 0) b.write(' AND ');
if (builder is DateTimeSqlExpressionBuilder ||
(builder is JsonSqlExpressionBuilder && builder.hasRaw)) {
if (tableName != null) b.write('$tableName.');
b.write(builder.compile());
} else {
b.write('$key ${builder.compile()}');
}
}
}
for (var other in _and) {
var sql = other.compile();
if (sql.isNotEmpty) b.write(' AND ($sql)');
}
for (var other in _not) {
var sql = other.compile();
if (sql.isNotEmpty) b.write(' NOT ($sql)');
}
for (var other in _or) {
var sql = other.compile();
if (sql.isNotEmpty) b.write(' OR ($sql)');
}
return b.toString();
}
}

View file

@ -0,0 +1,91 @@
import 'annotations.dart';
abstract class RelationshipType {
static const int hasMany = 0;
static const int hasOne = 1;
static const int belongsTo = 2;
static const int manyToMany = 3;
}
class Relationship {
final int type;
final String localKey;
final String foreignKey;
final String foreignTable;
final bool cascadeOnDelete;
final JoinType joinType;
const Relationship(this.type,
{this.localKey,
this.foreignKey,
this.foreignTable,
this.cascadeOnDelete,
this.joinType});
}
class HasMany extends Relationship {
const HasMany(
{String localKey,
String foreignKey,
String foreignTable,
bool cascadeOnDelete = false,
JoinType joinType})
: super(RelationshipType.hasMany,
localKey: localKey,
foreignKey: foreignKey,
foreignTable: foreignTable,
cascadeOnDelete: cascadeOnDelete == true,
joinType: joinType);
}
const HasMany hasMany = HasMany();
class HasOne extends Relationship {
const HasOne(
{String localKey,
String foreignKey,
String foreignTable,
bool cascadeOnDelete = false,
JoinType joinType})
: super(RelationshipType.hasOne,
localKey: localKey,
foreignKey: foreignKey,
foreignTable: foreignTable,
cascadeOnDelete: cascadeOnDelete == true,
joinType: joinType);
}
const HasOne hasOne = HasOne();
class BelongsTo extends Relationship {
const BelongsTo(
{String localKey,
String foreignKey,
String foreignTable,
JoinType joinType})
: super(RelationshipType.belongsTo,
localKey: localKey,
foreignKey: foreignKey,
foreignTable: foreignTable,
joinType: joinType);
}
const BelongsTo belongsTo = BelongsTo();
class ManyToMany extends Relationship {
final Type through;
const ManyToMany(this.through,
{String localKey,
String foreignKey,
String foreignTable,
bool cascadeOnDelete = false,
JoinType joinType})
: super(
RelationshipType.hasMany, // Many-to-Many is actually just a hasMany
localKey: localKey,
foreignKey: foreignKey,
foreignTable: foreignTable,
cascadeOnDelete: cascadeOnDelete == true,
joinType: joinType);
}

View file

@ -0,0 +1,37 @@
import 'query_base.dart';
/// Represents the `UNION` of two subqueries.
class Union<T> extends QueryBase<T> {
/// The subject(s) of this binary operation.
final QueryBase<T> left, right;
/// Whether this is a `UNION ALL` operation.
final bool all;
@override
final String tableName;
Union(this.left, this.right, {this.all = false, String tableName})
: this.tableName = tableName ?? left.tableName {
substitutionValues
..addAll(left.substitutionValues)
..addAll(right.substitutionValues);
}
@override
List<String> get fields => left.fields;
@override
T deserialize(List row) => left.deserialize(row);
@override
String compile(Set<String> trampoline,
{bool includeTableName = false,
String preamble,
bool withFields = true}) {
var selector = all == true ? 'UNION ALL' : 'UNION';
var t1 = Set<String>.from(trampoline);
var t2 = Set<String>.from(trampoline);
return '(${left.compile(t1, includeTableName: includeTableName)}) $selector (${right.compile(t2, includeTableName: includeTableName)})';
}
}

View file

@ -0,0 +1,3 @@
import 'package:charcode/ascii.dart';
bool isAscii(int ch) => ch >= $nul && ch <= $del;

View file

View file

@ -0,0 +1,19 @@
name: angel_orm
version: 2.1.0-beta.3
description: Runtime support for Angel's ORM. Includes base classes for queries.
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/orm
environment:
sdk: '>=2.0.0 <3.0.0'
dependencies:
charcode: ^1.0.0
intl: ^0.15.7
meta: ^1.0.0
string_scanner: ^1.0.0
dev_dependencies:
angel_model: ^1.0.0
angel_serialize: ^2.0.0
angel_serialize_generator: ^2.0.0
build_runner: ^1.0.0
pedantic: ^1.0.0
test: ^1.0.0

View file

@ -0,0 +1,57 @@
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
.packages
.pub/
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
*.g.part

View file

@ -0,0 +1,88 @@
# 2.1.0-beta.2
* Support for custom SQL expressions.
# 2.1.0-beta.1
* `OrmBuildContext` caching is now local to a `Builder`, so `watch`
*should* finally always run when required. Should resolve
[#85](https://github.com/angel-dart/orm/issues/85).
# 2.1.0-beta
* Relationships have always generated subqueries; now these subqueries are
available as `Query` objects on generated classes.
* Support explicitly-defined join types for relations.
# 2.0.5
* Remove `ShimFieldImpl` check, which broke relations.
* Fix bug where primary key type would not be emitted in migrations.
* Fix `ManyToMany` ignoring primary key types.
# 2.0.4
* Fix `reviveColumn` and element finding to properly detect all annotations now.
# 2.0.3
* Remove `targets` in `build.yaml`.
# 2.0.2
* Change `build_config` range to `">=0.3.0 <0.5.0"`.
# 2.0.1
* Gracefully handle `null` in enum fields.
* Add `take` to wherever `skip` is used.
# 2.0.0+2
* Widen `analyzer` dependency range.
# 2.0.0+1
* Restore `build.yaml`, which at some point, got deleted.
# 2.0.0
* `parse` -> `tryParse` where used.
# 2.0.0-dev.7
* Handle `@ManyToMany`.
* Handle cases where the class is not a `Model`.
* Stop assuming things have `id`, etc.
* Resolve a bug where the `indexType` of `@Column` annotations. would not be found.
* Add `cascade: true` to drops for hasOne/hasMany/ManyToMany migrations.
* Support enum default values in migrations.
# 2.0.0-dev.6
* Fix bug where an extra field would be inserted into joins and botch the result.
* Narrow analyzer dependency.
# 2.0.0-dev.5
* Implement cast-based `double` support.
* Finish `ListSqlExpressionBuilder`.
# 2.0.0-dev.4
* List generation support.
# 2.0.0-dev.3
* Add JSON/JSONB support for Maps.
# 2.0.0-dev.2
* Changes to work with `package:angel_orm@2.0.0-dev.15`.
# 2.0.0-dev.1
* Generate migration files.
# 2.0.0-dev
* Dart 2 updates, and more.
# 1.0.0-alpha+6
* `DateTime` is now `CAST` on insertion and update operations.
# 1.0.0-alpha+3
Implemented `@hasOne`, with tests. Still missing `@hasMany`.
`belongsToMany` will likely be scrapped.
# 1.0.0-alpha+2
* Added support for `belongsTo` relationships. Still missing `hasOne`, `hasMany`, `belongsToMany`.
# 1.0.0-alpha+1
* Closed #12. `insertX` and `updateX` now use `rc.camelCase`, instead of `rc.snakeCase`.
* Closed #13. Added `limit` and `offset` properties to `XQuery`.
* Closed #14. Refined the `or` method (it now takes an `XQueryWhere`), and removed `and` and `not`.
* Closed #16. Added `sortAscending` and `sortDescending` to `XQuery`.
* Closed #17. `delete` now uses `toSql` from `XQuery`.
* Closed #18. `XQuery` now supports `union` and `unionAll`.

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 The Angel Framework
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,8 @@
# angel_orm_generator
Source code generators for Angel's ORM.
This package can generate:
* A strongly-typed ORM
* SQL migration scripts
For documentation about the ORM, head to the main project repo:
https://github.com/angel-dart/orm

View file

@ -0,0 +1,4 @@
include: package:pedantic/analysis_options.yaml
analyzer:
strong-mode:
implicit-casts: false

View file

@ -0,0 +1,19 @@
builders:
angel_orm:
import: "package:angel_orm_generator/angel_orm_generator.dart"
builder_factories:
- migrationBuilder
- ormBuilder
auto_apply: root_package
build_to: cache
build_extensions:
.dart:
- ".angel_migration.g.part"
- ".angel_orm.g.part"
required_inputs:
- angel_serialize.g.part
- angel_serialize_serializer.g.part
applies_builders:
- angel_serialize_generator|angel_serialize
- source_gen|combining_builder
- source_gen|part_cleanup"

View file

@ -0,0 +1,52 @@
import 'dart:async';
import 'package:angel_migration/angel_migration.dart';
import 'package:angel_model/angel_model.dart';
import 'package:angel_orm/angel_orm.dart';
import 'package:angel_orm/src/query.dart';
import 'package:angel_serialize/angel_serialize.dart';
part 'main.g.dart';
main() async {
var query = EmployeeQuery()
..where.firstName.equals('Rich')
..where.lastName.equals('Person')
..orWhere((w) => w.salary.greaterThanOrEqualTo(75000))
..join('companies', 'company_id', 'id');
var richPerson = await query.getOne(_FakeExecutor());
print(richPerson.toJson());
}
class _FakeExecutor extends QueryExecutor {
const _FakeExecutor();
@override
Future<List<List>> query(
String tableName, String query, Map<String, dynamic> substitutionValues,
[returningFields]) async {
var now = DateTime.now();
print(
'_FakeExecutor received query: $query and values: $substitutionValues');
return [
[1, 'Rich', 'Person', 100000.0, now, now]
];
}
@override
Future<T> transaction<T>(FutureOr<T> Function(QueryExecutor) f) {
throw UnsupportedError('Transactions are not supported.');
}
}
@orm
@serializable
abstract class _Employee extends Model {
String get firstName;
String get lastName;
@Column(indexType: IndexType.unique)
String uniqueId;
double get salary;
}

View file

@ -0,0 +1,349 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'main.dart';
// **************************************************************************
// MigrationGenerator
// **************************************************************************
class EmployeeMigration extends Migration {
@override
up(Schema schema) {
schema.create('employees', (table) {
table.serial('id')..primaryKey();
table.timeStamp('created_at');
table.timeStamp('updated_at');
table.varChar('unique_id');
table.varChar('first_name');
table.varChar('last_name');
table.declare('salary', ColumnType('decimal'));
});
}
@override
down(Schema schema) {
schema.drop('employees');
}
}
// **************************************************************************
// OrmGenerator
// **************************************************************************
class EmployeeQuery extends Query<Employee, EmployeeQueryWhere> {
EmployeeQuery({Set<String> trampoline}) {
trampoline ??= Set();
trampoline.add(tableName);
_where = EmployeeQueryWhere(this);
}
@override
final EmployeeQueryValues values = EmployeeQueryValues();
EmployeeQueryWhere _where;
@override
get casts {
return {'salary': 'text'};
}
@override
get tableName {
return 'employees';
}
@override
get fields {
return const [
'id',
'created_at',
'updated_at',
'unique_id',
'first_name',
'last_name',
'salary'
];
}
@override
EmployeeQueryWhere get where {
return _where;
}
@override
EmployeeQueryWhere newWhereClause() {
return EmployeeQueryWhere(this);
}
static Employee parseRow(List row) {
if (row.every((x) => x == null)) return null;
var model = Employee(
id: (row[0] as String),
createdAt: (row[1] as DateTime),
updatedAt: (row[2] as DateTime),
uniqueId: (row[3] as String),
firstName: (row[4] as String),
lastName: (row[5] as String),
salary: double.tryParse(row[6].toString()));
return model;
}
@override
deserialize(List row) {
return parseRow(row);
}
}
class EmployeeQueryWhere extends QueryWhere {
EmployeeQueryWhere(EmployeeQuery query)
: id = StringSqlExpressionBuilder(query, 'id'),
createdAt = DateTimeSqlExpressionBuilder(query, 'created_at'),
updatedAt = DateTimeSqlExpressionBuilder(query, 'updated_at'),
uniqueId = StringSqlExpressionBuilder(query, 'unique_id'),
firstName = StringSqlExpressionBuilder(query, 'first_name'),
lastName = StringSqlExpressionBuilder(query, 'last_name'),
salary = NumericSqlExpressionBuilder<double>(query, 'salary');
final StringSqlExpressionBuilder id;
final DateTimeSqlExpressionBuilder createdAt;
final DateTimeSqlExpressionBuilder updatedAt;
final StringSqlExpressionBuilder uniqueId;
final StringSqlExpressionBuilder firstName;
final StringSqlExpressionBuilder lastName;
final NumericSqlExpressionBuilder<double> salary;
@override
get expressionBuilders {
return [id, createdAt, updatedAt, uniqueId, firstName, lastName, salary];
}
}
class EmployeeQueryValues extends MapQueryValues {
@override
get casts {
return {'salary': 'decimal'};
}
String get id {
return (values['id'] as String);
}
set id(String value) => values['id'] = value;
DateTime get createdAt {
return (values['created_at'] as DateTime);
}
set createdAt(DateTime value) => values['created_at'] = value;
DateTime get updatedAt {
return (values['updated_at'] as DateTime);
}
set updatedAt(DateTime value) => values['updated_at'] = value;
String get uniqueId {
return (values['unique_id'] as String);
}
set uniqueId(String value) => values['unique_id'] = value;
String get firstName {
return (values['first_name'] as String);
}
set firstName(String value) => values['first_name'] = value;
String get lastName {
return (values['last_name'] as String);
}
set lastName(String value) => values['last_name'] = value;
double get salary {
return double.tryParse((values['salary'] as String));
}
set salary(double value) => values['salary'] = value.toString();
void copyFrom(Employee model) {
id = model.id;
createdAt = model.createdAt;
updatedAt = model.updatedAt;
uniqueId = model.uniqueId;
firstName = model.firstName;
lastName = model.lastName;
salary = model.salary;
}
}
// **************************************************************************
// JsonModelGenerator
// **************************************************************************
@generatedSerializable
class Employee extends _Employee {
Employee(
{this.id,
this.createdAt,
this.updatedAt,
this.uniqueId,
this.firstName,
this.lastName,
this.salary});
/// A unique identifier corresponding to this item.
@override
String id;
/// The time at which this item was created.
@override
DateTime createdAt;
/// The last time at which this item was updated.
@override
DateTime updatedAt;
@override
String uniqueId;
@override
final String firstName;
@override
final String lastName;
@override
final double salary;
Employee copyWith(
{String id,
DateTime createdAt,
DateTime updatedAt,
String uniqueId,
String firstName,
String lastName,
double salary}) {
return Employee(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
uniqueId: uniqueId ?? this.uniqueId,
firstName: firstName ?? this.firstName,
lastName: lastName ?? this.lastName,
salary: salary ?? this.salary);
}
bool operator ==(other) {
return other is _Employee &&
other.id == id &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt &&
other.uniqueId == uniqueId &&
other.firstName == firstName &&
other.lastName == lastName &&
other.salary == salary;
}
@override
int get hashCode {
return hashObjects(
[id, createdAt, updatedAt, uniqueId, firstName, lastName, salary]);
}
@override
String toString() {
return "Employee(id=$id, createdAt=$createdAt, updatedAt=$updatedAt, uniqueId=$uniqueId, firstName=$firstName, lastName=$lastName, salary=$salary)";
}
Map<String, dynamic> toJson() {
return EmployeeSerializer.toMap(this);
}
}
// **************************************************************************
// SerializerGenerator
// **************************************************************************
const EmployeeSerializer employeeSerializer = EmployeeSerializer();
class EmployeeEncoder extends Converter<Employee, Map> {
const EmployeeEncoder();
@override
Map convert(Employee model) => EmployeeSerializer.toMap(model);
}
class EmployeeDecoder extends Converter<Map, Employee> {
const EmployeeDecoder();
@override
Employee convert(Map map) => EmployeeSerializer.fromMap(map);
}
class EmployeeSerializer extends Codec<Employee, Map> {
const EmployeeSerializer();
@override
get encoder => const EmployeeEncoder();
@override
get decoder => const EmployeeDecoder();
static Employee fromMap(Map map) {
return Employee(
id: map['id'] as String,
createdAt: map['created_at'] != null
? (map['created_at'] is DateTime
? (map['created_at'] as DateTime)
: DateTime.parse(map['created_at'].toString()))
: null,
updatedAt: map['updated_at'] != null
? (map['updated_at'] is DateTime
? (map['updated_at'] as DateTime)
: DateTime.parse(map['updated_at'].toString()))
: null,
uniqueId: map['unique_id'] as String,
firstName: map['first_name'] as String,
lastName: map['last_name'] as String,
salary: map['salary'] as double);
}
static Map<String, dynamic> toMap(_Employee model) {
if (model == null) {
return null;
}
return {
'id': model.id,
'created_at': model.createdAt?.toIso8601String(),
'updated_at': model.updatedAt?.toIso8601String(),
'unique_id': model.uniqueId,
'first_name': model.firstName,
'last_name': model.lastName,
'salary': model.salary
};
}
}
abstract class EmployeeFields {
static const List<String> allFields = <String>[
id,
createdAt,
updatedAt,
uniqueId,
firstName,
lastName,
salary
];
static const String id = 'id';
static const String createdAt = 'created_at';
static const String updatedAt = 'updated_at';
static const String uniqueId = 'unique_id';
static const String firstName = 'first_name';
static const String lastName = 'last_name';
static const String salary = 'salary';
}

View file

@ -0,0 +1,5 @@
//export 'src/mongodb_orm_generator.dart';
export 'src/migration_generator.dart';
export 'src/orm_build_context.dart';
export 'src/orm_generator.dart';
export 'src/readers.dart';

View file

@ -0,0 +1,309 @@
import 'dart:async';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:angel_model/angel_model.dart';
import 'package:angel_orm/angel_orm.dart';
import 'package:angel_serialize_generator/angel_serialize_generator.dart';
import 'package:build/build.dart';
import 'package:code_builder/code_builder.dart';
import 'package:dart_style/dart_style.dart';
import 'package:source_gen/source_gen.dart' hide LibraryBuilder;
import 'orm_build_context.dart';
Builder migrationBuilder(BuilderOptions options) {
return SharedPartBuilder([
MigrationGenerator(
autoSnakeCaseNames: options.config['auto_snake_case_names'] != false)
], 'angel_migration');
}
class MigrationGenerator extends GeneratorForAnnotation<Orm> {
static final Parameter _schemaParam = Parameter((b) => b
..name = 'schema'
..type = refer('Schema'));
static final Reference _schema = refer('schema');
/// If `true` (default), then field names will automatically be (de)serialized as snake_case.
final bool autoSnakeCaseNames;
const MigrationGenerator({this.autoSnakeCaseNames = true});
@override
Future<String> generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) async {
if (element is! ClassElement) {
throw 'Only classes can be annotated with @ORM().';
}
var generateMigrations =
annotation.peek('generateMigrations')?.boolValue ?? true;
if (!generateMigrations) {
return null;
}
var resolver = await buildStep.resolver;
var ctx = await buildOrmContext({}, element as ClassElement, annotation,
buildStep, resolver, autoSnakeCaseNames != false);
var lib = generateMigrationLibrary(
ctx, element as ClassElement, resolver, buildStep);
if (lib == null) return null;
return DartFormatter().format(lib.accept(DartEmitter()).toString());
}
Library generateMigrationLibrary(OrmBuildContext ctx, ClassElement element,
Resolver resolver, BuildStep buildStep) {
return Library((lib) {
lib.body.add(Class((clazz) {
clazz
..name = '${ctx.buildContext.modelClassName}Migration'
..extend = refer('Migration')
..methods
.addAll([buildUpMigration(ctx, lib), buildDownMigration(ctx)]);
}));
});
}
Method buildUpMigration(OrmBuildContext ctx, LibraryBuilder lib) {
return Method((meth) {
var autoIdAndDateFields = const TypeChecker.fromRuntime(Model)
.isAssignableFromType(ctx.buildContext.clazz.type);
meth
..name = 'up'
..annotations.add(refer('override'))
..requiredParameters.add(_schemaParam);
//var closure = Method.closure()..addPositional(parameter('table'));
var closure = Method((closure) {
closure
..requiredParameters.add(Parameter((b) => b..name = 'table'))
..body = Block((closureBody) {
var table = refer('table');
List<String> dup = [];
ctx.columns.forEach((name, col) {
// Skip custom-expression columns.
if (col.hasExpression) return;
var key = ctx.buildContext.resolveFieldName(name);
if (dup.contains(key)) {
return;
} else {
// if (key != 'id' || autoIdAndDateFields == false) {
// // Check for relationships that might duplicate
// for (var rName in ctx.relations.keys) {
// var relationship = ctx.relations[rName];
// if (relationship.localKey == key) return;
// }
// }
// Fix from: https://github.com/angel-dart/angel/issues/114#issuecomment-505525729
if (!(col.indexType == IndexType.primaryKey ||
(autoIdAndDateFields != false && name == 'id'))) {
// Check for relationships that might duplicate
for (var rName in ctx.relations.keys) {
var relationship = ctx.relations[rName];
if (relationship.localKey == key) return;
}
}
dup.add(key);
}
String methodName;
List<Expression> positional = [literal(key)];
Map<String, Expression> named = {};
if (autoIdAndDateFields != false && name == 'id') {
methodName = 'serial';
}
if (methodName == null) {
switch (col.type) {
case ColumnType.varChar:
methodName = 'varChar';
if (col.length != null) {
named['length'] = literal(col.length);
}
break;
case ColumnType.serial:
methodName = 'serial';
break;
case ColumnType.int:
methodName = 'integer';
break;
case ColumnType.float:
methodName = 'float';
break;
case ColumnType.numeric:
methodName = 'numeric';
break;
case ColumnType.boolean:
methodName = 'boolean';
break;
case ColumnType.date:
methodName = 'date';
break;
case ColumnType.dateTime:
methodName = 'dateTime';
break;
case ColumnType.timeStamp:
methodName = 'timeStamp';
break;
default:
Expression provColumn;
var colType = refer('Column');
var columnTypeType = refer('ColumnType');
if (col.length == null) {
methodName = 'declare';
provColumn = columnTypeType.newInstance([
literal(col.type.name),
]);
} else {
methodName = 'declareColumn';
provColumn = colType.newInstance([], {
'type': columnTypeType.newInstance([
literal(col.type.name),
]),
'length': literal(col.length),
});
}
positional.add(provColumn);
break;
}
}
var field = table.property(methodName).call(positional, named);
var cascade = <Expression>[];
var defaultValue = ctx.buildContext.defaults[name];
if (defaultValue != null && !defaultValue.isNull) {
var type = defaultValue.type;
Expression defaultExpr;
if (const TypeChecker.fromRuntime(RawSql)
.isAssignableFromType(defaultValue.type)) {
var value =
ConstantReader(defaultValue).read('value').stringValue;
defaultExpr =
refer('RawSql').constInstance([literalString(value)]);
} else if (type is InterfaceType && type.element.isEnum) {
// Default to enum index.
try {
var index =
ConstantReader(defaultValue).read('index')?.intValue;
if (index != null) defaultExpr = literalNum(index);
} catch (_) {
// Extremely weird error occurs here: `Not an instance of int`.
// Definitely an analyzer issue.
}
} else {
defaultExpr = CodeExpression(
Code(dartObjectToString(defaultValue)),
);
}
if (defaultExpr != null) {
cascade.add(refer('defaultsTo').call([defaultExpr]));
}
}
if (col.indexType == IndexType.primaryKey ||
(autoIdAndDateFields != false && name == 'id')) {
cascade.add(refer('primaryKey').call([]));
} else if (col.indexType == IndexType.unique) {
cascade.add(refer('unique').call([]));
}
if (col.isNullable != true) {
cascade.add(refer('notNull').call([]));
}
if (cascade.isNotEmpty) {
var b = StringBuffer()..writeln(field.accept(DartEmitter()));
for (var ex in cascade) {
b
..write('..')
..writeln(ex.accept(DartEmitter()));
}
field = CodeExpression(Code(b.toString()));
}
closureBody.addExpression(field);
});
ctx.relations.forEach((name, r) {
var relationship = r;
if (relationship.type == RelationshipType.belongsTo) {
// Fix from https://github.com/angel-dart/angel/issues/116#issuecomment-505546479
// var key = relationship.localKey;
// var field = table.property('integer').call([literal(key)]);
// // .references('user', 'id').onDeleteCascade()
var columnTypeType = refer('ColumnType');
var key = relationship.localKey;
var keyType = relationship
.foreign.columns[relationship.foreignKey].type.name;
var field = table.property('declare').call([
literal(key),
columnTypeType.newInstance([
literal(keyType),
])
]);
var ref = field.property('references').call([
literal(relationship.foreignTable),
literal(relationship.foreignKey),
]);
if (relationship.cascadeOnDelete != false &&
const [RelationshipType.hasOne, RelationshipType.belongsTo]
.contains(relationship.type)) {
ref = ref.property('onDeleteCascade').call([]);
}
closureBody.addExpression(ref);
}
});
});
});
meth.body = Block((b) {
b.addExpression(_schema.property('create').call([
literal(ctx.tableName),
closure.closure,
]));
});
});
}
Method buildDownMigration(OrmBuildContext ctx) {
return Method((b) {
b
..name = 'down'
..annotations.add(refer('override'))
..requiredParameters.add(_schemaParam)
..body = Block((b) {
var named = <String, Expression>{};
if (ctx.relations.values.any((r) =>
r.type == RelationshipType.hasOne ||
r.type == RelationshipType.hasMany ||
r.isManyToMany)) {
named['cascade'] = literalTrue;
}
b.addExpression(_schema
.property('drop')
.call([literalString(ctx.tableName)], named));
});
});
}
}

View file

@ -0,0 +1,428 @@
import 'dart:async';
import 'package:analyzer/dart/constant/value.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:angel_model/angel_model.dart';
import 'package:angel_orm/angel_orm.dart';
import 'package:angel_serialize/angel_serialize.dart';
import 'package:angel_serialize_generator/angel_serialize_generator.dart';
import 'package:angel_serialize_generator/build_context.dart';
import 'package:angel_serialize_generator/context.dart';
import 'package:build/build.dart';
import 'package:inflection2/inflection2.dart';
import 'package:recase/recase.dart';
import 'package:source_gen/source_gen.dart';
import 'readers.dart';
bool isHasRelation(Relationship r) =>
r.type == RelationshipType.hasOne || r.type == RelationshipType.hasMany;
bool isSpecialId(OrmBuildContext ctx, FieldElement field) {
return
// field is ShimFieldImpl &&
field is! RelationFieldImpl &&
(field.name == 'id' &&
const TypeChecker.fromRuntime(Model)
.isAssignableFromType(ctx.buildContext.clazz.type));
}
Element _findElement(FieldElement field) {
return (field.setter == null ? field.getter : field) ?? field;
}
FieldElement findPrimaryFieldInList(
OrmBuildContext ctx, Iterable<FieldElement> fields) {
for (var field_ in fields) {
var field = field_ is RelationFieldImpl ? field_.originalField : field_;
var element = _findElement(field);
// print(
// 'Searching in ${ctx.buildContext.originalClassName}=>${field?.name} (${field.runtimeType})');
// Check for column annotation...
var columnAnnotation = columnTypeChecker.firstAnnotationOf(element);
if (columnAnnotation != null) {
var column = reviveColumn(ConstantReader(columnAnnotation));
// print(
// ' * Found column on ${field.name} with indexType = ${column.indexType}');
// print(element.metadata);
if (column.indexType == IndexType.primaryKey) return field;
}
}
var specialId =
fields.firstWhere((f) => isSpecialId(ctx, f), orElse: () => null);
// print(
// 'Special ID on ${ctx.buildContext.originalClassName} => ${specialId?.name}');
return specialId;
}
Future<OrmBuildContext> buildOrmContext(
Map<String, OrmBuildContext> cache,
ClassElement clazz,
ConstantReader annotation,
BuildStep buildStep,
Resolver resolver,
bool autoSnakeCaseNames,
{bool heedExclude = true}) async {
// Check for @generatedSerializable
// ignore: unused_local_variable
DartObject generatedSerializable;
while ((generatedSerializable =
const TypeChecker.fromRuntime(GeneratedSerializable)
.firstAnnotationOf(clazz)) !=
null) {
clazz = clazz.supertype.element;
}
var id = clazz.location.components.join('-');
if (cache.containsKey(id)) {
return cache[id];
}
var buildCtx = await buildContext(
clazz, annotation, buildStep, resolver, autoSnakeCaseNames,
heedExclude: heedExclude);
var ormAnnotation = reviveORMAnnotation(annotation);
// print(
// 'tableName (${annotation.objectValue.type.name}) => ${ormAnnotation.tableName} from ${clazz.name} (${annotation.revive().namedArguments})');
var ctx = OrmBuildContext(
buildCtx,
ormAnnotation,
(ormAnnotation.tableName?.isNotEmpty == true)
? ormAnnotation.tableName
: pluralize(ReCase(clazz.name).snakeCase));
cache[id] = ctx;
// Read all fields
for (var field in buildCtx.fields) {
// Check for column annotation...
Column column;
var element = _findElement(field);
var columnAnnotation = columnTypeChecker.firstAnnotationOf(element);
// print('${element.name} => $columnAnnotation');
if (columnAnnotation != null) {
column = reviveColumn(ConstantReader(columnAnnotation));
}
if (column == null && isSpecialId(ctx, field)) {
// This is only for PostgreSQL, so implementations without a `serial` type
// must handle it accordingly, of course.
column = const Column(
type: ColumnType.serial, indexType: IndexType.primaryKey);
}
if (column == null) {
// Guess what kind of column this is...
column = Column(
type: inferColumnType(
buildCtx.resolveSerializedFieldType(field.name),
),
);
}
if (column != null && column.type == null) {
column = Column(
isNullable: column.isNullable,
length: column.length,
indexType: column.indexType,
type: inferColumnType(field.type),
);
}
// Try to find a relationship
var el = _findElement(field);
el ??= field;
var ann = relationshipTypeChecker.firstAnnotationOf(el);
if (ann != null) {
var cr = ConstantReader(ann);
var rc = ctx.buildContext.modelClassNameRecase;
var type = cr.read('type').intValue;
var localKey = cr.peek('localKey')?.stringValue;
var foreignKey = cr.peek('foreignKey')?.stringValue;
var foreignTable = cr.peek('foreignTable')?.stringValue;
var cascadeOnDelete = cr.peek('cascadeOnDelete')?.boolValue == true;
var through = cr.peek('through')?.typeValue;
OrmBuildContext foreign, throughContext;
if (foreignTable == null) {
// if (!isModelClass(field.type) &&
// !(field.type is InterfaceType &&
// isListOfModelType(field.type as InterfaceType))) {
var canUse = (field.type is InterfaceType &&
isListOfModelType(field.type as InterfaceType)) ||
isModelClass(field.type);
if (!canUse) {
throw UnsupportedError(
'Cannot apply relationship to field "${field.name}" - ${field.type} is not assignable to Model.');
} else {
try {
var refType = field.type;
if (refType is InterfaceType &&
const TypeChecker.fromRuntime(List)
.isAssignableFromType(refType) &&
refType.typeArguments.length == 1) {
refType = (refType as InterfaceType).typeArguments[0];
}
var modelType = firstModelAncestor(refType) ?? refType;
foreign = await buildOrmContext(
cache,
modelType.element as ClassElement,
ConstantReader(const TypeChecker.fromRuntime(Orm)
.firstAnnotationOf(modelType.element)),
buildStep,
resolver,
autoSnakeCaseNames);
// Resolve throughType as well
if (through != null && through is InterfaceType) {
throughContext = await buildOrmContext(
cache,
through.element,
ConstantReader(const TypeChecker.fromRuntime(Serializable)
.firstAnnotationOf(modelType.element)),
buildStep,
resolver,
autoSnakeCaseNames);
}
var ormAnn = const TypeChecker.fromRuntime(Orm)
.firstAnnotationOf(modelType.element);
if (ormAnn != null) {
foreignTable =
ConstantReader(ormAnn).peek('tableName')?.stringValue;
}
foreignTable ??=
pluralize(foreign.buildContext.modelClassNameRecase.snakeCase);
} on StackOverflowError {
throw UnsupportedError(
'There is an infinite cycle between ${clazz.name} and ${field.type.name}. This triggered a stack overflow.');
}
}
}
// Fill in missing keys
var rcc = ReCase(field.name);
String keyName(OrmBuildContext ctx, String missing) {
var _keyName =
findPrimaryFieldInList(ctx, ctx.buildContext.fields)?.name;
// print(
// 'Keyname for ${buildCtx.originalClassName}.${field.name} maybe = $_keyName??');
if (_keyName == null) {
throw '${ctx.buildContext.originalClassName} has no defined primary key, '
'so the relation on field ${buildCtx.originalClassName}.${field.name} must define a $missing.';
} else {
return _keyName;
}
}
if (type == RelationshipType.hasOne || type == RelationshipType.hasMany) {
localKey ??=
ctx.buildContext.resolveFieldName(keyName(ctx, 'local key'));
// print(
// 'Local key on ${buildCtx.originalClassName}.${field.name} defaulted to $localKey');
foreignKey ??= '${rc.snakeCase}_$localKey';
} else if (type == RelationshipType.belongsTo) {
foreignKey ??=
ctx.buildContext.resolveFieldName(keyName(foreign, 'foreign key'));
localKey ??= '${rcc.snakeCase}_$foreignKey';
}
// Figure out the join type.
var joinType = JoinType.left;
var joinTypeRdr = cr.peek('joinType')?.objectValue;
if (joinTypeRdr != null) {
// Unfortunately, the analyzer library provides little to nothing
// in the way of reading enums from source, so here's a hack.
var joinTypeType = (joinTypeRdr.type as InterfaceType);
var enumFields =
joinTypeType.element.fields.where((f) => f.isEnumConstant).toList();
for (int i = 0; i < enumFields.length; i++) {
if (enumFields[i].constantValue == joinTypeRdr) {
joinType = JoinType.values[i];
break;
}
}
}
var relation = RelationshipReader(
type,
localKey: localKey,
foreignKey: foreignKey,
foreignTable: foreignTable,
cascadeOnDelete: cascadeOnDelete,
through: through,
foreign: foreign,
throughContext: throughContext,
joinType: joinType,
);
// print('Relation on ${buildCtx.originalClassName}.${field.name} => '
// 'foreignKey=$foreignKey, localKey=$localKey');
if (relation.type == RelationshipType.belongsTo) {
var name = ReCase(relation.localKey).camelCase;
ctx.buildContext.aliases[name] = relation.localKey;
if (!ctx.effectiveFields.any((f) => f.name == field.name)) {
var foreignField = relation.findForeignField(ctx);
var foreign = relation.throughContext ?? relation.foreign;
var type = foreignField.type;
if (isSpecialId(foreign, foreignField)) {
type = field.type.element.context.typeProvider.intType;
}
var rf = RelationFieldImpl(name, relation, type, field);
ctx.effectiveFields.add(rf);
}
}
ctx.relations[field.name] = relation;
} else {
if (column?.type == null) {
throw 'Cannot infer SQL column type for field "${ctx.buildContext.originalClassName}.${field.name}" with type "${field.type.displayName}".';
}
// Expressions...
column = Column(
isNullable: column.isNullable,
length: column.length,
type: column.type,
indexType: column.indexType,
expression:
ConstantReader(columnAnnotation).peek('expression')?.stringValue,
);
ctx.columns[field.name] = column;
if (!ctx.effectiveFields.any((f) => f.name == field.name)) {
ctx.effectiveFields.add(field);
}
}
}
return ctx;
}
ColumnType inferColumnType(DartType type) {
if (const TypeChecker.fromRuntime(String).isAssignableFromType(type)) {
return ColumnType.varChar;
}
if (const TypeChecker.fromRuntime(int).isAssignableFromType(type)) {
return ColumnType.int;
}
if (const TypeChecker.fromRuntime(double).isAssignableFromType(type)) {
return ColumnType.decimal;
}
if (const TypeChecker.fromRuntime(num).isAssignableFromType(type)) {
return ColumnType.numeric;
}
if (const TypeChecker.fromRuntime(bool).isAssignableFromType(type)) {
return ColumnType.boolean;
}
if (const TypeChecker.fromRuntime(DateTime).isAssignableFromType(type)) {
return ColumnType.timeStamp;
}
if (const TypeChecker.fromRuntime(Map).isAssignableFromType(type)) {
return ColumnType.jsonb;
}
if (const TypeChecker.fromRuntime(List).isAssignableFromType(type)) {
return ColumnType.jsonb;
}
if (type is InterfaceType && type.element.isEnum) return ColumnType.int;
return null;
}
Column reviveColumn(ConstantReader cr) {
ColumnType columnType;
var indexTypeObj = cr.peek('indexType')?.objectValue;
indexTypeObj ??= cr.revive().namedArguments['indexType'];
var columnObj =
cr.peek('type')?.objectValue?.getField('name')?.toStringValue();
var indexType = IndexType.values[
indexTypeObj?.getField('index')?.toIntValue() ?? IndexType.none.index];
if (const TypeChecker.fromRuntime(PrimaryKey)
.isAssignableFromType(cr.objectValue.type)) {
indexType = IndexType.primaryKey;
}
if (columnObj != null) {
columnType = _ColumnType(columnObj);
}
return Column(
isNullable: cr.peek('isNullable')?.boolValue,
length: cr.peek('length')?.intValue,
type: columnType,
indexType: indexType,
);
}
const TypeChecker relationshipTypeChecker =
TypeChecker.fromRuntime(Relationship);
class OrmBuildContext {
final BuildContext buildContext;
final Orm ormAnnotation;
final String tableName;
final Map<String, Column> columns = {};
final List<FieldElement> effectiveFields = [];
final Map<String, RelationshipReader> relations = {};
OrmBuildContext(this.buildContext, this.ormAnnotation, this.tableName);
bool isNotCustomExprField(FieldElement field) {
var col = columns[field.name];
return col?.hasExpression != true;
}
Iterable<FieldElement> get effectiveNormalFields =>
effectiveFields.where(isNotCustomExprField);
}
class _ColumnType implements ColumnType {
@override
final String name;
_ColumnType(this.name);
}
class RelationFieldImpl extends ShimFieldImpl {
final FieldElement originalField;
final RelationshipReader relationship;
RelationFieldImpl(
String name, this.relationship, DartType type, this.originalField)
: super(name, type);
String get originalFieldName => originalField.name;
PropertyAccessorElement get getter => originalField.getter;
}
InterfaceType firstModelAncestor(DartType type) {
if (type is InterfaceType) {
if (type.superclass != null &&
const TypeChecker.fromRuntime(Model).isExactlyType(type.superclass)) {
return type;
} else {
return type.superclass == null
? null
: firstModelAncestor(type.superclass);
}
} else {
return null;
}
}

View file

@ -0,0 +1,736 @@
import 'dart:async';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:angel_orm/angel_orm.dart';
import 'package:angel_serialize_generator/angel_serialize_generator.dart';
import 'package:build/build.dart';
import 'package:code_builder/code_builder.dart' hide LibraryBuilder;
import 'package:source_gen/source_gen.dart';
import 'orm_build_context.dart';
var floatTypes = [
ColumnType.decimal,
ColumnType.float,
ColumnType.numeric,
ColumnType.real,
const ColumnType('double precision'),
];
Builder ormBuilder(BuilderOptions options) {
return SharedPartBuilder([
OrmGenerator(
autoSnakeCaseNames: options.config['auto_snake_case_names'] != false)
], 'angel_orm');
}
TypeReference futureOf(String type) {
return TypeReference((b) => b
..symbol = 'Future'
..types.add(refer(type)));
}
/// Builder that generates `.orm.g.dart`, with an abstract `FooOrm` class.
class OrmGenerator extends GeneratorForAnnotation<Orm> {
final bool autoSnakeCaseNames;
OrmGenerator({this.autoSnakeCaseNames});
@override
Future<String> generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) async {
if (element is ClassElement) {
var ctx = await buildOrmContext({}, element, annotation, buildStep,
buildStep.resolver, autoSnakeCaseNames);
var lib = buildOrmLibrary(buildStep.inputId, ctx);
return lib.accept(DartEmitter()).toString();
} else {
throw 'The @Orm() annotation can only be applied to classes.';
}
}
Library buildOrmLibrary(AssetId inputId, OrmBuildContext ctx) {
return Library((lib) {
// Create `FooQuery` class
// Create `FooQueryWhere` class
lib.body.add(buildQueryClass(ctx));
lib.body.add(buildWhereClass(ctx));
lib.body.add(buildValuesClass(ctx));
});
}
Class buildQueryClass(OrmBuildContext ctx) {
return Class((clazz) {
var rc = ctx.buildContext.modelClassNameRecase;
var queryWhereType = refer('${rc.pascalCase}QueryWhere');
clazz
..name = '${rc.pascalCase}Query'
..extend = TypeReference((b) {
b
..symbol = 'Query'
..types.addAll([
ctx.buildContext.modelClassType,
queryWhereType,
]);
});
// Override casts so that we can cast doubles
clazz.methods.add(Method((b) {
b
..name = 'casts'
..annotations.add(refer('override'))
..type = MethodType.getter
..body = Block((b) {
var args = <String, Expression>{};
for (var field in ctx.effectiveFields) {
var name = ctx.buildContext.resolveFieldName(field.name);
var type = ctx.columns[field.name]?.type;
if (type == null) continue;
if (floatTypes.contains(type)) {
args[name] = literalString('text');
}
}
b.addExpression(literalMap(args).returned);
});
}));
// Add values
clazz.fields.add(Field((b) {
var type = refer('${rc.pascalCase}QueryValues');
b
..name = 'values'
..modifier = FieldModifier.final$
..annotations.add(refer('override'))
..type = type
..assignment = type.newInstance([]).code;
}));
// Add tableName
clazz.methods.add(Method((m) {
m
..name = 'tableName'
..annotations.add(refer('override'))
..type = MethodType.getter
..body = Block((b) {
b.addExpression(literalString(ctx.tableName).returned);
});
}));
// Add fields getter
clazz.methods.add(Method((m) {
m
..name = 'fields'
..annotations.add(refer('override'))
..type = MethodType.getter
..body = Block((b) {
var names = ctx.effectiveFields
.map((f) =>
literalString(ctx.buildContext.resolveFieldName(f.name)))
.toList();
b.addExpression(literalConstList(names).returned);
});
}));
// Add _where member
clazz.fields.add(Field((b) {
b
..name = '_where'
..type = queryWhereType;
}));
// Add where getter
clazz.methods.add(Method((b) {
b
..name = 'where'
..type = MethodType.getter
..returns = queryWhereType
..annotations.add(refer('override'))
..body = Block((b) => b.addExpression(refer('_where').returned));
}));
// newWhereClause()
clazz.methods.add(Method((b) {
b
..name = 'newWhereClause'
..annotations.add(refer('override'))
..returns = queryWhereType
..body = Block((b) => b.addExpression(
queryWhereType.newInstance([refer('this')]).returned));
}));
// Add deserialize()
clazz.methods.add(Method((m) {
m
..name = 'parseRow'
..static = true
..returns = ctx.buildContext.modelClassType
..requiredParameters.add(Parameter((b) => b
..name = 'row'
..type = refer('List')))
..body = Block((b) {
int i = 0;
var args = <String, Expression>{};
for (var field in ctx.effectiveFields) {
var fType = field.type;
Reference type = convertTypeReference(field.type);
if (isSpecialId(ctx, field)) type = refer('int');
var expr = (refer('row').index(literalNum(i++)));
if (isSpecialId(ctx, field)) {
expr = expr.property('toString').call([]);
} else if (field is RelationFieldImpl) {
continue;
} else if (ctx.columns[field.name]?.type == ColumnType.json) {
expr = refer('json')
.property('decode')
.call([expr.asA(refer('String'))]).asA(type);
} else if (floatTypes.contains(ctx.columns[field.name]?.type)) {
expr = refer('double')
.property('tryParse')
.call([expr.property('toString').call([])]);
} else if (fType is InterfaceType && fType.element.isEnum) {
var isNull = expr.equalTo(literalNull);
expr = isNull.conditional(literalNull,
type.property('values').index(expr.asA(refer('int'))));
} else {
expr = expr.asA(type);
}
args[field.name] = expr;
}
b.statements
.add(Code('if (row.every((x) => x == null)) return null;'));
b.addExpression(ctx.buildContext.modelClassType
.newInstance([], args).assignVar('model'));
ctx.relations.forEach((name, relation) {
if (!const [
RelationshipType.hasOne,
RelationshipType.belongsTo,
RelationshipType.hasMany
].contains(relation.type)) return;
var foreign = relation.foreign;
var skipToList = refer('row')
.property('skip')
.call([literalNum(i)])
.property('take')
.call([literalNum(relation.foreign.effectiveFields.length)])
.property('toList')
.call([]);
var parsed = refer(
'${foreign.buildContext.modelClassNameRecase.pascalCase}Query')
.property('parseRow')
.call([skipToList]);
if (relation.type == RelationshipType.hasMany) {
parsed = literalList([parsed]);
var pp = parsed.accept(DartEmitter());
parsed = CodeExpression(
Code('$pp.where((x) => x != null).toList()'));
}
var expr =
refer('model').property('copyWith').call([], {name: parsed});
var block =
Block((b) => b.addExpression(refer('model').assign(expr)));
var blockStr = block.accept(DartEmitter());
var ifStr = 'if (row.length > $i) { $blockStr }';
b.statements.add(Code(ifStr));
i += relation.foreign.effectiveFields.length;
});
b.addExpression(refer('model').returned);
});
}));
clazz.methods.add(Method((m) {
m
..name = 'deserialize'
..annotations.add(refer('override'))
..requiredParameters.add(Parameter((b) => b
..name = 'row'
..type = refer('List')))
..body = Block((b) {
b.addExpression(refer('parseRow').call([refer('row')]).returned);
});
}));
// If there are any relations, we need some overrides.
clazz.constructors.add(Constructor((b) {
b
..optionalParameters.add(Parameter((b) => b
..named = true
..name = 'parent'
..type = refer('Query')))
..optionalParameters.add(Parameter((b) => b
..named = true
..name = 'trampoline'
..type = TypeReference((b) => b
..symbol = 'Set'
..types.add(refer('String')))))
..initializers.add(Code('super(parent: parent)'))
..body = Block((b) {
b.statements.addAll([
Code('trampoline ??= Set();'),
Code('trampoline.add(tableName);'),
]);
// Add any manual SQL expressions.
ctx.columns.forEach((name, col) {
if (col != null && col.hasExpression) {
var lhs = refer('expressions').index(
literalString(ctx.buildContext.resolveFieldName(name)));
var rhs = literalString(col.expression);
b.addExpression(lhs.assign(rhs));
}
});
// Add a constructor that initializes _where
b.addExpression(
refer('_where')
.assign(queryWhereType.newInstance([refer('this')])),
);
// Note: this is where subquery fields for relations are added.
ctx.relations.forEach((fieldName, relation) {
//var name = ctx.buildContext.resolveFieldName(fieldName);
if (relation.type == RelationshipType.belongsTo ||
relation.type == RelationshipType.hasOne ||
relation.type == RelationshipType.hasMany) {
var foreign = relation.throughContext ?? relation.foreign;
// If this is a many-to-many, add the fields from the other object.
var additionalStrs = relation.foreign.effectiveFields.map((f) =>
relation.foreign.buildContext.resolveFieldName(f.name));
var additionalFields = additionalStrs.map(literalString);
var joinArgs = [relation.localKey, relation.foreignKey]
.map(literalString)
.toList();
// In the case of a many-to-many, we don't generate a subquery field,
// as it easily leads to stack overflows.
if (relation.isManyToMany) {
// We can't simply join against the "through" table; this itself must
// be a join.
// (SELECT role_users.role_id, <user_fields>
// FROM users
// LEFT JOIN role_users ON role_users.user_id=users.id)
var foreignFields = additionalStrs
.map((f) => '${relation.foreign.tableName}.$f');
var b = StringBuffer('(SELECT ');
// role_users.role_id
b.write('${relation.throughContext.tableName}');
b.write('.${relation.foreignKey}');
// , <user_fields>
b.write(foreignFields.isEmpty
? ''
: ', ' + foreignFields.join(', '));
// FROM users
b.write(' FROM ');
b.write(relation.foreign.tableName);
// LEFT JOIN role_users
b.write(' LEFT JOIN ${relation.throughContext.tableName}');
// Figure out which field on the "through" table points to users (foreign).
var throughRelation =
relation.throughContext.relations.values.firstWhere((e) {
return e.foreignTable == relation.foreign.tableName;
}, orElse: () {
// _Role has a many-to-many to _User through _RoleUser, but
// _RoleUser has no relation pointing to _User.
var b = StringBuffer();
b.write(ctx.buildContext.modelClassName);
b.write('has a many-to-many relationship to ');
b.write(relation.foreign.buildContext.modelClassName);
b.write(' through ');
b.write(
relation.throughContext.buildContext.modelClassName);
b.write(', but ');
b.write(
relation.throughContext.buildContext.modelClassName);
b.write('has no relation pointing to ');
b.write(relation.foreign.buildContext.modelClassName);
b.write('.');
throw b.toString();
});
// ON role_users.user_id=users.id)
b.write(' ON ');
b.write('${relation.throughContext.tableName}');
b.write('.');
b.write(throughRelation.localKey);
b.write('=');
b.write(relation.foreign.tableName);
b.write('.');
b.write(throughRelation.foreignKey);
b.write(')');
joinArgs.insert(0, literalString(b.toString()));
} else {
// In the past, we would either do a join on the table name
// itself, or create an instance of a query.
//
// From this point on, however, we will create a field for each
// join, so that users can customize the generated query.
//
// There'll be a private `_field`, and then a getter, named `field`,
// that returns the subquery object.
var foreignQueryType = refer(
foreign.buildContext.modelClassNameRecase.pascalCase +
'Query');
clazz
..fields.add(Field((b) => b
..name = '_$fieldName'
..type = foreignQueryType))
..methods.add(Method((b) => b
..name = fieldName
..type = MethodType.getter
..returns = foreignQueryType
..body = refer('_$fieldName').returned.statement));
// Assign a value to `_field`.
var queryInstantiation = foreignQueryType.newInstance([], {
'trampoline': refer('trampoline'),
'parent': refer('this')
});
joinArgs.insert(
0, refer('_$fieldName').assign(queryInstantiation));
}
var joinType = relation.joinTypeString;
b.addExpression(refer(joinType).call(joinArgs, {
'additionalFields':
literalConstList(additionalFields.toList()),
'trampoline': refer('trampoline'),
}));
}
});
});
}));
// If we have any many-to-many relations, we need to prevent
// fetching this table within their joins.
var manyToMany = ctx.relations.entries.where((e) => e.value.isManyToMany);
if (manyToMany.isNotEmpty) {
var outExprs = manyToMany.map<Expression>((e) {
var foreignTableName = e.value.throughContext.tableName;
return CodeExpression(Code('''
(!(
trampoline.contains('${ctx.tableName}')
&& trampoline.contains('$foreignTableName')
))
'''));
});
var out = outExprs.reduce((a, b) => a.and(b));
clazz.methods.add(Method((b) {
b
..name = 'canCompile'
..annotations.add(refer('override'))
..requiredParameters.add(Parameter((b) => b..name = 'trampoline'))
..returns = refer('bool')
..body = Block((b) {
b.addExpression(out.returned);
});
}));
}
// Also, if there is a @HasMany, generate overrides for query methods that
// execute in a transaction, and invoke fetchLinked.
if (ctx.relations.values.any((r) => r.type == RelationshipType.hasMany)) {
for (var methodName in const ['get', 'update', 'delete']) {
clazz.methods.add(Method((b) {
var type = ctx.buildContext.modelClassType.accept(DartEmitter());
b
..name = methodName
..annotations.add(refer('override'))
..requiredParameters.add(Parameter((b) => b
..name = 'executor'
..type = refer('QueryExecutor')));
// Collect hasMany options, and ultimately merge them
var merge = <String>[];
ctx.relations.forEach((name, relation) {
if (relation.type == RelationshipType.hasMany) {
// This is only allowed with lists.
var field =
ctx.buildContext.fields.firstWhere((f) => f.name == name);
var typeLiteral =
convertTypeReference(field.type).accept(DartEmitter());
merge.add('''
$name: $typeLiteral.from(l.$name ?? [])..addAll(model.$name ?? [])
''');
}
});
var merged = merge.join(', ');
var keyName =
findPrimaryFieldInList(ctx, ctx.buildContext.fields)?.name;
if (keyName == null) {
throw '${ctx.buildContext.originalClassName} has no defined primary key.\n'
'@HasMany and @ManyToMany relations require a primary key to be defined on the model.';
}
b.body = Code('''
return super.$methodName(executor).then((result) {
return result.fold<List<$type>>([], (out, model) {
var idx = out.indexWhere((m) => m.$keyName == model.$keyName);
if (idx == -1) {
return out..add(model);
} else {
var l = out[idx];
return out..[idx] = l.copyWith($merged);
}
});
});
''');
}));
}
}
});
}
Class buildWhereClass(OrmBuildContext ctx) {
return Class((clazz) {
var rc = ctx.buildContext.modelClassNameRecase;
clazz
..name = '${rc.pascalCase}QueryWhere'
..extend = refer('QueryWhere');
// Build expressionBuilders getter
clazz.methods.add(Method((m) {
m
..name = 'expressionBuilders'
..annotations.add(refer('override'))
..type = MethodType.getter
..body = Block((b) {
var references =
ctx.effectiveNormalFields.map((f) => refer(f.name));
b.addExpression(literalList(references).returned);
});
}));
var initializers = <Code>[];
// Add builders for each field
for (var field in ctx.effectiveNormalFields) {
var name = field.name;
var args = <Expression>[];
DartType type;
Reference builderType;
try {
type = ctx.buildContext.resolveSerializedFieldType(field.name);
} on StateError {
type = field.type;
}
if (const TypeChecker.fromRuntime(int).isExactlyType(type) ||
const TypeChecker.fromRuntime(double).isExactlyType(type) ||
isSpecialId(ctx, field)) {
builderType = TypeReference((b) => b
..symbol = 'NumericSqlExpressionBuilder'
..types.add(refer(isSpecialId(ctx, field) ? 'int' : type.name)));
} else if (type is InterfaceType && type.element.isEnum) {
builderType = TypeReference((b) => b
..symbol = 'EnumSqlExpressionBuilder'
..types.add(convertTypeReference(type)));
args.add(CodeExpression(Code('(v) => v.index')));
} else if (const TypeChecker.fromRuntime(String).isExactlyType(type)) {
builderType = refer('StringSqlExpressionBuilder');
} else if (const TypeChecker.fromRuntime(bool).isExactlyType(type)) {
builderType = refer('BooleanSqlExpressionBuilder');
} else if (const TypeChecker.fromRuntime(DateTime)
.isExactlyType(type)) {
builderType = refer('DateTimeSqlExpressionBuilder');
} else if (const TypeChecker.fromRuntime(Map)
.isAssignableFromType(type)) {
builderType = refer('MapSqlExpressionBuilder');
} else if (const TypeChecker.fromRuntime(List)
.isAssignableFromType(type)) {
builderType = refer('ListSqlExpressionBuilder');
} else if (ctx.relations.containsKey(field.name)) {
var relation = ctx.relations[field.name];
if (relation.type != RelationshipType.belongsTo) {
continue;
} else {
builderType = TypeReference((b) => b
..symbol = 'NumericSqlExpressionBuilder'
..types.add(refer('int')));
name = relation.localKey;
}
} else {
throw UnsupportedError(
'Cannot generate ORM code for field of type ${field.type.name}.');
}
clazz.fields.add(Field((b) {
b
..name = name
..modifier = FieldModifier.final$
..type = builderType;
initializers.add(
refer(field.name)
.assign(builderType.newInstance([
refer('query'),
literalString(ctx.buildContext.resolveFieldName(field.name))
].followedBy(args)))
.code,
);
}));
}
// Now, just add a constructor that initializes each builder.
clazz.constructors.add(Constructor((b) {
b
..requiredParameters.add(Parameter((b) => b
..name = 'query'
..type = refer('${rc.pascalCase}Query')))
..initializers.addAll(initializers);
}));
});
}
Class buildValuesClass(OrmBuildContext ctx) {
return Class((clazz) {
var rc = ctx.buildContext.modelClassNameRecase;
clazz
..name = '${rc.pascalCase}QueryValues'
..extend = refer('MapQueryValues');
// Override casts so that we can cast Lists
clazz.methods.add(Method((b) {
b
..name = 'casts'
..annotations.add(refer('override'))
..type = MethodType.getter
..body = Block((b) {
var args = <String, Expression>{};
for (var field in ctx.effectiveFields) {
var fType = field.type;
var name = ctx.buildContext.resolveFieldName(field.name);
var type = ctx.columns[field.name]?.type;
if (type == null) continue;
if (const TypeChecker.fromRuntime(List)
.isAssignableFromType(fType)) {
args[name] = literalString(type.name);
} else if (floatTypes.contains(type)) {
args[name] = literalString(type.name);
}
}
b.addExpression(literalMap(args).returned);
});
}));
// Each field generates a getter and setter
for (var field in ctx.effectiveNormalFields) {
var fType = field.type;
var name = ctx.buildContext.resolveFieldName(field.name);
var type = convertTypeReference(field.type);
clazz.methods.add(Method((b) {
var value = refer('values').index(literalString(name));
if (fType is InterfaceType && fType.element.isEnum) {
var asInt = value.asA(refer('int'));
var t = convertTypeReference(fType);
value = t.property('values').index(asInt);
} else if (const TypeChecker.fromRuntime(List)
.isAssignableFromType(fType)) {
value = refer('json')
.property('decode')
.call([value.asA(refer('String'))]).asA(refer('List'));
} else if (floatTypes.contains(ctx.columns[field.name]?.type)) {
value = refer('double')
.property('tryParse')
.call([value.asA(refer('String'))]);
} else {
value = value.asA(type);
}
b
..name = field.name
..type = MethodType.getter
..returns = type
..body = Block((b) => b.addExpression(value.returned));
}));
clazz.methods.add(Method((b) {
Expression value = refer('value');
if (fType is InterfaceType && fType.element.isEnum) {
value = CodeExpression(Code('value?.index'));
} else if (const TypeChecker.fromRuntime(List)
.isAssignableFromType(fType)) {
value = refer('json').property('encode').call([value]);
} else if (floatTypes.contains(ctx.columns[field.name]?.type)) {
value = value.property('toString').call([]);
}
b
..name = field.name
..type = MethodType.setter
..requiredParameters.add(Parameter((b) => b
..name = 'value'
..type = type))
..body =
refer('values').index(literalString(name)).assign(value).code;
}));
}
// Add an copyFrom(model)
clazz.methods.add(Method((b) {
b
..name = 'copyFrom'
..returns = refer('void')
..requiredParameters.add(Parameter((b) => b
..name = 'model'
..type = ctx.buildContext.modelClassType))
..body = Block((b) {
for (var field in ctx.effectiveNormalFields) {
if (isSpecialId(ctx, field) || field is RelationFieldImpl) {
continue;
}
b.addExpression(refer(field.name)
.assign(refer('model').property(field.name)));
}
for (var field in ctx.effectiveNormalFields) {
if (field is RelationFieldImpl) {
var original = field.originalFieldName;
var prop = refer('model').property(original);
// Add only if present
var target = refer('values').index(literalString(
ctx.buildContext.resolveFieldName(field.name)));
var foreign = field.relationship.throughContext ??
field.relationship.foreign;
var foreignField = field.relationship.findForeignField(ctx);
var parsedId = prop.property(foreignField.name);
if (isSpecialId(foreign, field)) {
parsedId =
(refer('int').property('tryParse').call([parsedId]));
}
var cond = prop.notEqualTo(literalNull);
var condStr = cond.accept(DartEmitter());
var blkStr =
Block((b) => b.addExpression(target.assign(parsedId)))
.accept(DartEmitter());
var ifStmt = Code('if ($condStr) { $blkStr }');
b.statements.add(ifStmt);
}
}
});
}));
});
}
}

View file

@ -0,0 +1,87 @@
import 'package:analyzer/dart/constant/value.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:angel_orm/angel_orm.dart';
import 'package:source_gen/source_gen.dart';
import 'orm_build_context.dart';
const TypeChecker columnTypeChecker = TypeChecker.fromRuntime(Column);
Orm reviveORMAnnotation(ConstantReader reader) {
return Orm(
tableName: reader.peek('tableName')?.stringValue,
generateMigrations: reader.peek('generateMigrations')?.boolValue ?? true);
}
class ColumnReader {
final ConstantReader reader;
ColumnReader(this.reader);
bool get isNullable => reader.peek('isNullable')?.boolValue ?? true;
int get length => reader.peek('length')?.intValue;
DartObject get defaultValue => reader.peek('defaultValue')?.objectValue;
}
class RelationshipReader {
final int type;
final String localKey;
final String foreignKey;
final String foreignTable;
final bool cascadeOnDelete;
final DartType through;
final OrmBuildContext foreign;
final OrmBuildContext throughContext;
final JoinType joinType;
const RelationshipReader(this.type,
{this.localKey,
this.foreignKey,
this.foreignTable,
this.cascadeOnDelete,
this.through,
this.foreign,
this.throughContext,
this.joinType});
bool get isManyToMany =>
type == RelationshipType.hasMany && throughContext != null;
String get joinTypeString {
switch (joinType ?? JoinType.left) {
case JoinType.inner:
return 'join';
case JoinType.left:
return 'leftJoin';
case JoinType.right:
return 'rightJoin';
case JoinType.full:
return 'fullOuterJoin';
case JoinType.self:
return 'selfJoin';
default:
return 'join';
}
}
FieldElement findLocalField(OrmBuildContext ctx) {
return ctx.effectiveFields.firstWhere(
(f) => ctx.buildContext.resolveFieldName(f.name) == localKey,
orElse: () {
throw '${ctx.buildContext.clazz.name} has no field that maps to the name "$localKey", '
'but it has a @HasMany() relation that expects such a field.';
});
}
FieldElement findForeignField(OrmBuildContext ctx) {
var foreign = throughContext ?? this.foreign;
return foreign.effectiveFields.firstWhere(
(f) => foreign.buildContext.resolveFieldName(f.name) == foreignKey,
orElse: () {
throw '${foreign.buildContext.clazz.name} has no field that maps to the name "$foreignKey", '
'but ${ctx.buildContext.clazz.name} has a @HasMany() relation that expects such a field.';
});
}
}

View file

@ -0,0 +1,37 @@
name: angel_orm_generator
version: 2.1.0-beta.2
description: Code generators for Angel's ORM. Generates query builder classes.
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/orm
environment:
sdk: ">=2.0.0<3.0.0"
dependencies:
analyzer: ">=0.35.0 <2.0.0"
angel_model: ^1.0.0
angel_serialize: ^2.0.0
angel_orm: ^2.1.0-beta
angel_serialize_generator: ^2.0.0
build: ^1.0.0
build_config: ^0.4.0
code_builder: ^3.0.0
dart_style: ^1.0.0
inflection2: ^0.4.2
meta: ^1.0.0
path: ^1.0.0
recase: ^2.0.0
source_gen: ^0.9.0
dev_dependencies:
angel_framework: ^2.0.0-alpha
angel_migration:
path: ../angel_migration
#angel_test: ^1.0.0
build_runner: ^1.0.0
collection: ^1.0.0
pedantic: ^1.0.0
postgres: ^1.0.0
test: ^1.0.0
# dependency_overrides:
# angel_orm:
# path: ../angel_orm
# angel_serialize_generator:
# path: ../../serialize/angel_serialize_generator

View file

@ -0,0 +1,43 @@
import 'package:angel_migration/angel_migration.dart';
import 'package:angel_model/angel_model.dart';
import 'package:angel_orm/angel_orm.dart';
import 'package:angel_orm_mysql/angel_orm_mysql.dart';
import 'package:angel_serialize/angel_serialize.dart';
import 'package:logging/logging.dart';
import 'package:sqljocky5/sqljocky.dart';
part 'main.g.dart';
main() async {
hierarchicalLoggingEnabled = true;
Logger.root
..level = Level.ALL
..onRecord.listen(print);
var settings = ConnectionSettings(
db: 'angel_orm_test', user: 'angel_orm_test', password: 'angel_orm_test');
var connection = await MySqlConnection.connect(settings);
var logger = Logger('angel_orm_mysql');
var executor = MySqlExecutor(connection, logger: logger);
var query = TodoQuery();
query.values
..text = 'Clean your room!'
..isComplete = false;
var todo = await query.insert(executor);
print(todo.toJson());
var query2 = TodoQuery()..where.id.equals(todo.idAsInt);
var todo2 = await query2.getOne(executor);
print(todo2.toJson());
print(todo == todo2);
}
@serializable
@orm
abstract class _Todo extends Model {
String get text;
@DefaultsTo(false)
bool isComplete;
}

View file

@ -0,0 +1,273 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'main.dart';
// **************************************************************************
// MigrationGenerator
// **************************************************************************
class TodoMigration extends Migration {
@override
up(Schema schema) {
schema.create('todos', (table) {
table.serial('id')..primaryKey();
table.boolean('is_complete')..defaultsTo(false);
table.varChar('text');
table.timeStamp('created_at');
table.timeStamp('updated_at');
});
}
@override
down(Schema schema) {
schema.drop('todos');
}
}
// **************************************************************************
// OrmGenerator
// **************************************************************************
class TodoQuery extends Query<Todo, TodoQueryWhere> {
TodoQuery({Set<String> trampoline}) {
trampoline ??= Set();
trampoline.add(tableName);
_where = TodoQueryWhere(this);
}
@override
final TodoQueryValues values = TodoQueryValues();
TodoQueryWhere _where;
@override
get casts {
return {};
}
@override
get tableName {
return 'todos';
}
@override
get fields {
return const ['id', 'is_complete', 'text', 'created_at', 'updated_at'];
}
@override
TodoQueryWhere get where {
return _where;
}
@override
TodoQueryWhere newWhereClause() {
return TodoQueryWhere(this);
}
static Todo parseRow(List row) {
if (row.every((x) => x == null)) return null;
var model = Todo(
id: row[0].toString(),
isComplete: (row[1] as bool),
text: (row[2] as String),
createdAt: (row[3] as DateTime),
updatedAt: (row[4] as DateTime));
return model;
}
@override
deserialize(List row) {
return parseRow(row);
}
}
class TodoQueryWhere extends QueryWhere {
TodoQueryWhere(TodoQuery query)
: id = NumericSqlExpressionBuilder<int>(query, 'id'),
isComplete = BooleanSqlExpressionBuilder(query, 'is_complete'),
text = StringSqlExpressionBuilder(query, 'text'),
createdAt = DateTimeSqlExpressionBuilder(query, 'created_at'),
updatedAt = DateTimeSqlExpressionBuilder(query, 'updated_at');
final NumericSqlExpressionBuilder<int> id;
final BooleanSqlExpressionBuilder isComplete;
final StringSqlExpressionBuilder text;
final DateTimeSqlExpressionBuilder createdAt;
final DateTimeSqlExpressionBuilder updatedAt;
@override
get expressionBuilders {
return [id, isComplete, text, createdAt, updatedAt];
}
}
class TodoQueryValues extends MapQueryValues {
@override
get casts {
return {};
}
int get id {
return (values['id'] as int);
}
set id(int value) => values['id'] = value;
bool get isComplete {
return (values['is_complete'] as bool);
}
set isComplete(bool value) => values['is_complete'] = value;
String get text {
return (values['text'] as String);
}
set text(String value) => values['text'] = value;
DateTime get createdAt {
return (values['created_at'] as DateTime);
}
set createdAt(DateTime value) => values['created_at'] = value;
DateTime get updatedAt {
return (values['updated_at'] as DateTime);
}
set updatedAt(DateTime value) => values['updated_at'] = value;
void copyFrom(Todo model) {
isComplete = model.isComplete;
text = model.text;
createdAt = model.createdAt;
updatedAt = model.updatedAt;
}
}
// **************************************************************************
// JsonModelGenerator
// **************************************************************************
@generatedSerializable
class Todo extends _Todo {
Todo(
{this.id,
this.isComplete = false,
this.text,
this.createdAt,
this.updatedAt});
@override
final String id;
@override
final bool isComplete;
@override
final String text;
@override
final DateTime createdAt;
@override
final DateTime updatedAt;
Todo copyWith(
{String id,
bool isComplete,
String text,
DateTime createdAt,
DateTime updatedAt}) {
return new Todo(
id: id ?? this.id,
isComplete: isComplete ?? this.isComplete,
text: text ?? this.text,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt);
}
bool operator ==(other) {
return other is _Todo &&
other.id == id &&
other.isComplete == isComplete &&
other.text == text &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt;
}
@override
int get hashCode {
return hashObjects([id, isComplete, text, createdAt, updatedAt]);
}
Map<String, dynamic> toJson() {
return TodoSerializer.toMap(this);
}
}
// **************************************************************************
// SerializerGenerator
// **************************************************************************
abstract class TodoSerializer {
static Todo fromMap(Map map) {
if (map['is_complete'] == null) {
throw new FormatException(
"Missing required field 'is_complete' on Todo.");
}
return new Todo(
id: map['id'] as String,
isComplete: map['is_complete'] as bool ?? false,
text: map['text'] as String,
createdAt: map['created_at'] != null
? (map['created_at'] is DateTime
? (map['created_at'] as DateTime)
: DateTime.parse(map['created_at'].toString()))
: null,
updatedAt: map['updated_at'] != null
? (map['updated_at'] is DateTime
? (map['updated_at'] as DateTime)
: DateTime.parse(map['updated_at'].toString()))
: null);
}
static Map<String, dynamic> toMap(_Todo model) {
if (model == null) {
return null;
}
if (model.isComplete == null) {
throw new FormatException(
"Missing required field 'is_complete' on Todo.");
}
return {
'id': model.id,
'is_complete': model.isComplete,
'text': model.text,
'created_at': model.createdAt?.toIso8601String(),
'updated_at': model.updatedAt?.toIso8601String()
};
}
}
abstract class TodoFields {
static const List<String> allFields = <String>[
id,
isComplete,
text,
createdAt,
updatedAt
];
static const String id = 'id';
static const String isComplete = 'is_complete';
static const String text = 'text';
static const String createdAt = 'created_at';
static const String updatedAt = 'updated_at';
}

View file

@ -0,0 +1,91 @@
import 'dart:async';
import 'package:angel_orm/angel_orm.dart';
import 'package:logging/logging.dart';
// import 'package:pool/pool.dart';
import 'package:sqljocky5/connection/connection.dart';
import 'package:sqljocky5/sqljocky.dart';
class MySqlExecutor extends QueryExecutor {
/// An optional [Logger] to write to.
final Logger logger;
final Querier _connection;
MySqlExecutor(this._connection, {this.logger});
Future<void> close() {
if (_connection is MySqlConnection) {
return (_connection as MySqlConnection).close();
} else {
return Future.value();
}
}
Future<Transaction> _startTransaction() {
if (_connection is Transaction) {
return Future.value(_connection as Transaction);
} else if (_connection is MySqlConnection) {
return (_connection as MySqlConnection).begin();
} else {
throw StateError('Connection must be transaction or connection');
}
}
@override
Future<List<List>> query(
String tableName, String query, Map<String, dynamic> substitutionValues,
[List<String> returningFields]) {
// Change @id -> ?
for (var name in substitutionValues.keys) {
query = query.replaceAll('@$name', '?');
}
logger?.fine('Query: $query');
logger?.fine('Values: $substitutionValues');
if (returningFields?.isNotEmpty != true) {
return _connection
.prepared(query, substitutionValues.values)
.then((results) => results.map((r) => r.toList()).toList());
} else {
return Future(() async {
var tx = await _startTransaction();
try {
var writeResults =
await tx.prepared(query, substitutionValues.values);
var fieldSet = returningFields.map((s) => '`$s`').join(',');
var fetchSql = 'select $fieldSet from $tableName where id = ?;';
logger?.fine(fetchSql);
var readResults =
await tx.prepared(fetchSql, [writeResults.insertId]);
var mapped = readResults.map((r) => r.toList()).toList();
await tx.commit();
return mapped;
} catch (_) {
await tx?.rollback();
rethrow;
}
});
}
}
@override
Future<T> transaction<T>(FutureOr<T> Function(QueryExecutor) f) async {
if (_connection is Transaction) {
return await f(this);
}
Transaction tx;
try {
tx = await _startTransaction();
var executor = MySqlExecutor(tx, logger: logger);
var result = await f(executor);
await tx.commit();
return result;
} catch (_) {
await tx?.rollback();
rethrow;
}
}
}

View file

@ -0,0 +1,24 @@
name: angel_orm_mysql
version: 0.0.0
description: MySQL support for Angel's ORM. Includes functionality for querying and transactions.
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/orm
environment:
sdk: '>=2.0.0 <3.0.0'
dependencies:
angel_orm: ^2.1.0-beta
logging: ^0.11.0
pool: ^1.0.0
sqljocky5: ^2.0.0
dev_dependencies:
angel_migration: ^2.0.0
angel_orm_generator: ^2.1.0-beta
angel_orm_test:
path: ../angel_orm_test
build_runner: ^1.0.0
test: ^1.0.0
dependency_overrides:
angel_migration:
path: ../angel_migration
angel_orm_generator:
path: ../angel_orm_generator

View file

@ -0,0 +1,31 @@
import 'package:angel_orm_test/angel_orm_test.dart';
import 'package:logging/logging.dart';
import 'package:test/test.dart';
import 'common.dart';
void main() {
Logger.root.onRecord.listen((rec) {
print(rec);
if (rec.error != null) print(rec.error);
if (rec.stackTrace != null) print(rec.stackTrace);
});
group('postgresql', () {
group('belongsTo',
() => belongsToTests(my(['author', 'book']), close: closeMy));
group(
'edgeCase',
() => edgeCaseTests(my(['unorthodox', 'weird_join', 'song', 'numba']),
close: closeMy));
group('enumAndNested',
() => enumAndNestedTests(my(['has_car']), close: closeMy));
group('hasMany', () => hasManyTests(my(['tree', 'fruit']), close: closeMy));
group('hasMap', () => hasMapTests(my(['has_map']), close: closeMy));
group('hasOne', () => hasOneTests(my(['leg', 'foot']), close: closeMy));
group(
'manyToMany',
() =>
manyToManyTests(my(['user', 'role', 'user_role']), close: closeMy));
group('standalone', () => standaloneTests(my(['car']), close: closeMy));
});
}

View file

@ -0,0 +1,28 @@
import 'dart:async';
import 'dart:io';
import 'package:angel_orm/angel_orm.dart';
import 'package:angel_orm_mysql/angel_orm_mysql.dart';
import 'package:logging/logging.dart';
import 'package:sqljocky5/sqljocky.dart';
FutureOr<QueryExecutor> Function() my(Iterable<String> schemas) {
return () => connectToMySql(schemas);
}
Future<void> closeMy(QueryExecutor executor) =>
(executor as MySqlExecutor).close();
Future<MySqlExecutor> connectToMySql(Iterable<String> schemas) async {
var settings = ConnectionSettings(
db: 'angel_orm_test',
user: Platform.environment['MYSQL_USERNAME'] ?? 'angel_orm_test',
password: Platform.environment['MYSQL_PASSWORD'] ?? 'angel_orm_test');
var connection = await MySqlConnection.connect(settings);
var logger = Logger('angel_orm_mysql');
for (var s in schemas)
await connection
.execute(await new File('test/migrations/$s.sql').readAsString());
return MySqlExecutor(connection, logger: logger);
}

View file

@ -0,0 +1,6 @@
CREATE TEMPORARY TABLE "authors" (
id serial PRIMARY KEY,
name varchar(255) UNIQUE NOT NULL,
created_at timestamp,
updated_at timestamp
);

View file

@ -0,0 +1,8 @@
CREATE TEMPORARY TABLE "books" (
id serial PRIMARY KEY,
author_id int NOT NULL,
partner_author_id int,
name varchar(255),
created_at timestamp,
updated_at timestamp
);

View file

@ -0,0 +1,9 @@
CREATE TEMPORARY TABLE "cars" (
id serial PRIMARY KEY,
make varchar(255) NOT NULL,
description TEXT NOT NULL,
family_friendly BOOLEAN NOT NULL,
recalled_at timestamp,
created_at timestamp,
updated_at timestamp
);

View file

@ -0,0 +1,7 @@
CREATE TEMPORARY TABLE "feet" (
id serial PRIMARY KEY,
leg_id int NOT NULL,
n_toes int NOT NULL,
created_at timestamp,
updated_at timestamp
);

View file

@ -0,0 +1,8 @@
CREATE TEMPORARY TABLE "fruits" (
"id" serial,
"tree_id" int,
"common_name" varchar,
"created_at" timestamp,
"updated_at" timestamp,
PRIMARY KEY(id)
);

View file

@ -0,0 +1,6 @@
CREATE TEMPORARY TABLE "has_cars" (
id serial PRIMARY KEY,
type int not null,
created_at timestamp,
updated_at timestamp
);

View file

@ -0,0 +1,7 @@
CREATE TEMPORARY TABLE "has_maps" (
id serial PRIMARY KEY,
value jsonb not null,
list jsonb not null,
created_at timestamp,
updated_at timestamp
);

View file

@ -0,0 +1,6 @@
CREATE TEMPORARY TABLE "legs" (
id serial PRIMARY KEY,
name varchar(255) NOT NULL,
created_at timestamp,
updated_at timestamp
);

View file

@ -0,0 +1,7 @@
CREATE TEMPORARY TABLE "numbas" (
"i" int,
"parent" int references weird_joins(id),
created_at TIMESTAMP,
updated_at TIMESTAMP,
PRIMARY KEY(i)
);

View file

@ -0,0 +1,6 @@
CREATE TEMPORARY TABLE "roles" (
"id" serial PRIMARY KEY,
"name" varchar(255),
"created_at" timestamp,
"updated_at" timestamp
);

View file

@ -0,0 +1,8 @@
CREATE TEMPORARY TABLE "songs" (
"id" serial,
"weird_join_id" int references weird_joins(id),
"title" varchar(255),
created_at TIMESTAMP,
updated_at TIMESTAMP,
PRIMARY KEY(id)
);

View file

@ -0,0 +1,8 @@
CREATE TEMPORARY TABLE "trees" (
"id" serial,
"rings" smallint UNIQUE,
"created_at" timestamp,
"updated_at" timestamp,
UNIQUE(rings),
PRIMARY KEY(id)
);

Some files were not shown because too many files have changed in this diff Show more