Add 'packages/orm/' from commit 'ceb58a25b58eeabeeab5a0bb6257f144e150dc24'
git-subtree-dir: packages/orm git-subtree-mainline:edfd785dfe
git-subtree-split:ceb58a25b5
This commit is contained in:
commit
6db839928b
185 changed files with 16826 additions and 0 deletions
57
packages/orm/.gitignore
vendored
Normal file
57
packages/orm/.gitignore
vendored
Normal 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
1
packages/orm/.idea/.name
Normal file
|
@ -0,0 +1 @@
|
|||
orm
|
451
packages/orm/.idea/dbnavigator.xml
Normal file
451
packages/orm/.idea/dbnavigator.xml
Normal 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>
|
8
packages/orm/.idea/modules.xml
Normal file
8
packages/orm/.idea/modules.xml
Normal 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>
|
23
packages/orm/.idea/orm.iml
Normal file
23
packages/orm/.idea/orm.iml
Normal 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>
|
|
@ -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>
|
6
packages/orm/.idea/vcs.xml
Normal file
6
packages/orm/.idea/vcs.xml
Normal 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
10
packages/orm/.travis.yml
Normal 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
267
packages/orm/README.md
Normal file
|
@ -0,0 +1,267 @@
|
|||
# orm
|
||||
[](https://pub.dartlang.org/packages/angel_orm)
|
||||
[](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
12
packages/orm/angel_migration/.gitignore
vendored
Executable 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/
|
11
packages/orm/angel_migration/CHANGELOG.md
Executable file
11
packages/orm/angel_migration/CHANGELOG.md
Executable 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.
|
21
packages/orm/angel_migration/LICENSE
Executable file
21
packages/orm/angel_migration/LICENSE
Executable 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.
|
2
packages/orm/angel_migration/README.md
Executable file
2
packages/orm/angel_migration/README.md
Executable file
|
@ -0,0 +1,2 @@
|
|||
# migration
|
||||
A PostgreSQL database migration framework built on Angel's ORM.
|
3
packages/orm/angel_migration/analysis_options.yaml
Executable file
3
packages/orm/angel_migration/analysis_options.yaml
Executable file
|
@ -0,0 +1,3 @@
|
|||
analyzer:
|
||||
strong-mode:
|
||||
implicit-casts: false
|
42
packages/orm/angel_migration/example/todo.dart
Executable file
42
packages/orm/angel_migration/example/todo.dart
Executable 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');
|
||||
}
|
||||
}
|
4
packages/orm/angel_migration/lib/angel_migration.dart
Executable file
4
packages/orm/angel_migration/lib/angel_migration.dart
Executable file
|
@ -0,0 +1,4 @@
|
|||
export 'src/column.dart';
|
||||
export 'src/migration.dart';
|
||||
export 'src/schema.dart';
|
||||
export 'src/table.dart';
|
85
packages/orm/angel_migration/lib/src/column.dart
Executable file
85
packages/orm/angel_migration/lib/src/column.dart
Executable 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';
|
||||
}
|
||||
}
|
6
packages/orm/angel_migration/lib/src/migration.dart
Executable file
6
packages/orm/angel_migration/lib/src/migration.dart
Executable file
|
@ -0,0 +1,6 @@
|
|||
import 'schema.dart';
|
||||
|
||||
abstract class Migration {
|
||||
void up(Schema schema);
|
||||
void down(Schema schema);
|
||||
}
|
15
packages/orm/angel_migration/lib/src/schema.dart
Executable file
15
packages/orm/angel_migration/lib/src/schema.dart
Executable 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));
|
||||
}
|
46
packages/orm/angel_migration/lib/src/table.dart
Executable file
46
packages/orm/angel_migration/lib/src/table.dart
Executable 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);
|
||||
}
|
0
packages/orm/angel_migration/mono_pkg.yaml
Executable file
0
packages/orm/angel_migration/mono_pkg.yaml
Executable file
9
packages/orm/angel_migration/pubspec.yaml
Executable file
9
packages/orm/angel_migration/pubspec.yaml
Executable 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
|
13
packages/orm/angel_migration_runner/.gitignore
vendored
Executable file
13
packages/orm/angel_migration_runner/.gitignore
vendored
Executable 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
|
32
packages/orm/angel_migration_runner/CHANGELOG.md
Executable file
32
packages/orm/angel_migration_runner/CHANGELOG.md
Executable 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.
|
21
packages/orm/angel_migration_runner/LICENSE
Executable file
21
packages/orm/angel_migration_runner/LICENSE
Executable 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.
|
2
packages/orm/angel_migration_runner/README.md
Executable file
2
packages/orm/angel_migration_runner/README.md
Executable file
|
@ -0,0 +1,2 @@
|
|||
# migration
|
||||
A PostgreSQL database migration framework built on Angel's ORM.
|
3
packages/orm/angel_migration_runner/analysis_options.yaml
Executable file
3
packages/orm/angel_migration_runner/analysis_options.yaml
Executable file
|
@ -0,0 +1,3 @@
|
|||
analyzer:
|
||||
strong-mode:
|
||||
implicit-casts: false
|
33
packages/orm/angel_migration_runner/example/main.dart
Executable file
33
packages/orm/angel_migration_runner/example/main.dart
Executable 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');
|
||||
}
|
14
packages/orm/angel_migration_runner/example/todo.sql
Executable file
14
packages/orm/angel_migration_runner/example/todo.sql
Executable 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
|
||||
);
|
2
packages/orm/angel_migration_runner/lib/angel_migration_runner.dart
Executable file
2
packages/orm/angel_migration_runner/lib/angel_migration_runner.dart
Executable file
|
@ -0,0 +1,2 @@
|
|||
export 'src/cli.dart';
|
||||
export 'src/runner.dart';
|
3
packages/orm/angel_migration_runner/lib/postgres.dart
Executable file
3
packages/orm/angel_migration_runner/lib/postgres.dart
Executable file
|
@ -0,0 +1,3 @@
|
|||
export 'src/postgres/runner.dart';
|
||||
export 'src/postgres/schema.dart';
|
||||
export 'src/postgres/table.dart';
|
70
packages/orm/angel_migration_runner/lib/src/cli.dart
Executable file
70
packages/orm/angel_migration_runner/lib/src/cli.dart
Executable 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();
|
||||
}
|
||||
}
|
138
packages/orm/angel_migration_runner/lib/src/postgres/runner.dart
Executable file
138
packages/orm/angel_migration_runner/lib/src/postgres/runner.dart
Executable 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();
|
||||
}
|
||||
}
|
58
packages/orm/angel_migration_runner/lib/src/postgres/schema.dart
Executable file
58
packages/orm/angel_migration_runner/lib/src/postgres/schema.dart
Executable 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);
|
||||
}
|
166
packages/orm/angel_migration_runner/lib/src/postgres/table.dart
Executable file
166
packages/orm/angel_migration_runner/lib/src/postgres/table.dart
Executable 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"');
|
||||
}
|
||||
}
|
14
packages/orm/angel_migration_runner/lib/src/runner.dart
Executable file
14
packages/orm/angel_migration_runner/lib/src/runner.dart
Executable 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();
|
||||
}
|
14
packages/orm/angel_migration_runner/lib/src/util.dart
Executable file
14
packages/orm/angel_migration_runner/lib/src/util.dart
Executable 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);
|
||||
}
|
0
packages/orm/angel_migration_runner/mono_pkg.yaml
Executable file
0
packages/orm/angel_migration_runner/mono_pkg.yaml
Executable file
13
packages/orm/angel_migration_runner/pubspec.yaml
Executable file
13
packages/orm/angel_migration_runner/pubspec.yaml
Executable 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
58
packages/orm/angel_orm/.gitignore
vendored
Normal 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
|
146
packages/orm/angel_orm/CHANGELOG.md
Normal file
146
packages/orm/angel_orm/CHANGELOG.md
Normal 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`.
|
21
packages/orm/angel_orm/LICENSE
Normal file
21
packages/orm/angel_orm/LICENSE
Normal 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.
|
6
packages/orm/angel_orm/README.md
Normal file
6
packages/orm/angel_orm/README.md
Normal 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
|
4
packages/orm/angel_orm/analysis_options.yaml
Normal file
4
packages/orm/angel_orm/analysis_options.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
include: package:pedantic/analysis_options.yaml
|
||||
analyzer:
|
||||
strong-mode:
|
||||
implicit-casts: false
|
67
packages/orm/angel_orm/example/main.angel_serialize.g.part
Normal file
67
packages/orm/angel_orm/example/main.angel_serialize.g.part
Normal 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);
|
||||
}
|
||||
}
|
112
packages/orm/angel_orm/example/main.dart
Normal file
112
packages/orm/angel_orm/example/main.dart
Normal 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;
|
||||
}
|
71
packages/orm/angel_orm/example/main.g.dart
Normal file
71
packages/orm/angel_orm/example/main.g.dart
Normal 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);
|
||||
}
|
||||
}
|
64
packages/orm/angel_orm/example/main.serializer.g.dart
Normal file
64
packages/orm/angel_orm/example/main.serializer.g.dart
Normal 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';
|
||||
}
|
15
packages/orm/angel_orm/lib/angel_orm.dart
Normal file
15
packages/orm/angel_orm/lib/angel_orm.dart
Normal 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';
|
31
packages/orm/angel_orm/lib/src/annotations.dart
Normal file
31
packages/orm/angel_orm/lib/src/annotations.dart
Normal 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 }
|
651
packages/orm/angel_orm/lib/src/builder.dart
Normal file
651
packages/orm/angel_orm/lib/src/builder.dart
Normal 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));
|
||||
}
|
||||
}
|
71
packages/orm/angel_orm/lib/src/join_builder.dart
Normal file
71
packages/orm/angel_orm/lib/src/join_builder.dart
Normal 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();
|
||||
}
|
||||
}
|
8
packages/orm/angel_orm/lib/src/join_on.dart
Normal file
8
packages/orm/angel_orm/lib/src/join_on.dart
Normal file
|
@ -0,0 +1,8 @@
|
|||
import 'builder.dart';
|
||||
|
||||
class JoinOn {
|
||||
final SqlExpressionBuilder key;
|
||||
final SqlExpressionBuilder value;
|
||||
|
||||
JoinOn(this.key, this.value);
|
||||
}
|
9
packages/orm/angel_orm/lib/src/map_query_values.dart
Normal file
9
packages/orm/angel_orm/lib/src/map_query_values.dart
Normal 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;
|
||||
}
|
129
packages/orm/angel_orm/lib/src/migration.dart
Normal file
129
packages/orm/angel_orm/lib/src/migration.dart
Normal 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');
|
||||
}
|
8
packages/orm/angel_orm/lib/src/order_by.dart
Normal file
8
packages/orm/angel_orm/lib/src/order_by.dart
Normal 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';
|
||||
}
|
382
packages/orm/angel_orm/lib/src/query.dart
Normal file
382
packages/orm/angel_orm/lib/src/query.dart
Normal 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);
|
||||
}
|
||||
}
|
58
packages/orm/angel_orm/lib/src/query_base.dart
Normal file
58
packages/orm/angel_orm/lib/src/query_base.dart
Normal 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);
|
||||
}
|
||||
}
|
23
packages/orm/angel_orm/lib/src/query_executor.dart
Normal file
23
packages/orm/angel_orm/lib/src/query_executor.dart
Normal 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);
|
||||
}
|
59
packages/orm/angel_orm/lib/src/query_values.dart
Normal file
59
packages/orm/angel_orm/lib/src/query_values.dart
Normal 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();
|
||||
}
|
||||
}
|
59
packages/orm/angel_orm/lib/src/query_where.dart
Normal file
59
packages/orm/angel_orm/lib/src/query_where.dart
Normal 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();
|
||||
}
|
||||
}
|
91
packages/orm/angel_orm/lib/src/relations.dart
Normal file
91
packages/orm/angel_orm/lib/src/relations.dart
Normal 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);
|
||||
}
|
37
packages/orm/angel_orm/lib/src/union.dart
Normal file
37
packages/orm/angel_orm/lib/src/union.dart
Normal 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)})';
|
||||
}
|
||||
}
|
3
packages/orm/angel_orm/lib/src/util.dart
Normal file
3
packages/orm/angel_orm/lib/src/util.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
import 'package:charcode/ascii.dart';
|
||||
|
||||
bool isAscii(int ch) => ch >= $nul && ch <= $del;
|
0
packages/orm/angel_orm/mono_pkg.yaml
Normal file
0
packages/orm/angel_orm/mono_pkg.yaml
Normal file
19
packages/orm/angel_orm/pubspec.yaml
Normal file
19
packages/orm/angel_orm/pubspec.yaml
Normal 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
|
57
packages/orm/angel_orm_generator/.gitignore
vendored
Normal file
57
packages/orm/angel_orm_generator/.gitignore
vendored
Normal 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
|
88
packages/orm/angel_orm_generator/CHANGELOG.md
Normal file
88
packages/orm/angel_orm_generator/CHANGELOG.md
Normal 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`.
|
21
packages/orm/angel_orm_generator/LICENSE
Normal file
21
packages/orm/angel_orm_generator/LICENSE
Normal 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.
|
8
packages/orm/angel_orm_generator/README.md
Normal file
8
packages/orm/angel_orm_generator/README.md
Normal 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
|
4
packages/orm/angel_orm_generator/analysis_options.yaml
Normal file
4
packages/orm/angel_orm_generator/analysis_options.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
include: package:pedantic/analysis_options.yaml
|
||||
analyzer:
|
||||
strong-mode:
|
||||
implicit-casts: false
|
19
packages/orm/angel_orm_generator/build.yaml
Normal file
19
packages/orm/angel_orm_generator/build.yaml
Normal 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"
|
52
packages/orm/angel_orm_generator/example/main.dart
Normal file
52
packages/orm/angel_orm_generator/example/main.dart
Normal 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;
|
||||
}
|
349
packages/orm/angel_orm_generator/example/main.g.dart
Normal file
349
packages/orm/angel_orm_generator/example/main.g.dart
Normal 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';
|
||||
}
|
|
@ -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';
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
428
packages/orm/angel_orm_generator/lib/src/orm_build_context.dart
Normal file
428
packages/orm/angel_orm_generator/lib/src/orm_build_context.dart
Normal 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;
|
||||
}
|
||||
}
|
736
packages/orm/angel_orm_generator/lib/src/orm_generator.dart
Normal file
736
packages/orm/angel_orm_generator/lib/src/orm_generator.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
87
packages/orm/angel_orm_generator/lib/src/readers.dart
Normal file
87
packages/orm/angel_orm_generator/lib/src/readers.dart
Normal 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.';
|
||||
});
|
||||
}
|
||||
}
|
0
packages/orm/angel_orm_generator/mono_pkg.yaml
Normal file
0
packages/orm/angel_orm_generator/mono_pkg.yaml
Normal file
37
packages/orm/angel_orm_generator/pubspec.yaml
Normal file
37
packages/orm/angel_orm_generator/pubspec.yaml
Normal 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
|
43
packages/orm/angel_orm_mysql/example/main.dart
Normal file
43
packages/orm/angel_orm_mysql/example/main.dart
Normal 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;
|
||||
}
|
273
packages/orm/angel_orm_mysql/example/main.g.dart
Normal file
273
packages/orm/angel_orm_mysql/example/main.g.dart
Normal 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';
|
||||
}
|
91
packages/orm/angel_orm_mysql/lib/angel_orm_mysql.dart
Normal file
91
packages/orm/angel_orm_mysql/lib/angel_orm_mysql.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
0
packages/orm/angel_orm_mysql/mono_pkg.yaml
Normal file
0
packages/orm/angel_orm_mysql/mono_pkg.yaml
Normal file
24
packages/orm/angel_orm_mysql/pubspec.yaml
Normal file
24
packages/orm/angel_orm_mysql/pubspec.yaml
Normal 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
|
31
packages/orm/angel_orm_mysql/test/all_test.dart
Normal file
31
packages/orm/angel_orm_mysql/test/all_test.dart
Normal 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));
|
||||
});
|
||||
}
|
28
packages/orm/angel_orm_mysql/test/common.dart
Normal file
28
packages/orm/angel_orm_mysql/test/common.dart
Normal 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);
|
||||
}
|
6
packages/orm/angel_orm_mysql/test/migrations/author.sql
Normal file
6
packages/orm/angel_orm_mysql/test/migrations/author.sql
Normal 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
|
||||
);
|
8
packages/orm/angel_orm_mysql/test/migrations/book.sql
Normal file
8
packages/orm/angel_orm_mysql/test/migrations/book.sql
Normal 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
|
||||
);
|
9
packages/orm/angel_orm_mysql/test/migrations/car.sql
Normal file
9
packages/orm/angel_orm_mysql/test/migrations/car.sql
Normal 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
|
||||
);
|
7
packages/orm/angel_orm_mysql/test/migrations/foot.sql
Normal file
7
packages/orm/angel_orm_mysql/test/migrations/foot.sql
Normal 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
|
||||
);
|
8
packages/orm/angel_orm_mysql/test/migrations/fruit.sql
Normal file
8
packages/orm/angel_orm_mysql/test/migrations/fruit.sql
Normal 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)
|
||||
);
|
6
packages/orm/angel_orm_mysql/test/migrations/has_car.sql
Normal file
6
packages/orm/angel_orm_mysql/test/migrations/has_car.sql
Normal file
|
@ -0,0 +1,6 @@
|
|||
CREATE TEMPORARY TABLE "has_cars" (
|
||||
id serial PRIMARY KEY,
|
||||
type int not null,
|
||||
created_at timestamp,
|
||||
updated_at timestamp
|
||||
);
|
7
packages/orm/angel_orm_mysql/test/migrations/has_map.sql
Normal file
7
packages/orm/angel_orm_mysql/test/migrations/has_map.sql
Normal 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
|
||||
);
|
6
packages/orm/angel_orm_mysql/test/migrations/leg.sql
Normal file
6
packages/orm/angel_orm_mysql/test/migrations/leg.sql
Normal file
|
@ -0,0 +1,6 @@
|
|||
CREATE TEMPORARY TABLE "legs" (
|
||||
id serial PRIMARY KEY,
|
||||
name varchar(255) NOT NULL,
|
||||
created_at timestamp,
|
||||
updated_at timestamp
|
||||
);
|
7
packages/orm/angel_orm_mysql/test/migrations/numba.sql
Normal file
7
packages/orm/angel_orm_mysql/test/migrations/numba.sql
Normal 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)
|
||||
);
|
6
packages/orm/angel_orm_mysql/test/migrations/role.sql
Normal file
6
packages/orm/angel_orm_mysql/test/migrations/role.sql
Normal file
|
@ -0,0 +1,6 @@
|
|||
CREATE TEMPORARY TABLE "roles" (
|
||||
"id" serial PRIMARY KEY,
|
||||
"name" varchar(255),
|
||||
"created_at" timestamp,
|
||||
"updated_at" timestamp
|
||||
);
|
8
packages/orm/angel_orm_mysql/test/migrations/song.sql
Normal file
8
packages/orm/angel_orm_mysql/test/migrations/song.sql
Normal 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)
|
||||
);
|
8
packages/orm/angel_orm_mysql/test/migrations/tree.sql
Normal file
8
packages/orm/angel_orm_mysql/test/migrations/tree.sql
Normal 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
Loading…
Reference in a new issue