motion_record 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,111 @@
1
+ module MotionRecord
2
+ module Schema
3
+ class ColumnDefinition
4
+ TYPE_MAP = {
5
+ :integer => "INTEGER",
6
+ :text => "TEXT",
7
+ :float => "REAL"
8
+ }
9
+ INVERSE_TYPE_MAP = {
10
+ "INTEGER" => :integer,
11
+ "TEXT" => :text,
12
+ "REAL" => :float
13
+ }
14
+
15
+ attr_reader :type
16
+ attr_reader :name
17
+ attr_reader :options
18
+
19
+ # name - name of the column
20
+ # type - a Symbol representing the column type
21
+ # options - Hash of constraints for the column:
22
+ # :primary - set to true to configure as the primary auto-incrementing key
23
+ # :null - set to false to add a "NOT NULL" constraint
24
+ # :default - TODO
25
+ def initialize(type, name, options)
26
+ @type = type.to_sym
27
+ @name = name.to_sym
28
+ @options = options
29
+ end
30
+
31
+ def to_sql_definition
32
+ [@name, sql_type, sql_options].compact.join(" ")
33
+ end
34
+
35
+ def default
36
+ @options[:default]
37
+ end
38
+
39
+ # Build a new ColumnDefinition from the result of a "PRAGMA table_info()"
40
+ # query
41
+ #
42
+ # pragma - Hash representing a row of the query's result:
43
+ # :cid - column index
44
+ # :name - column name
45
+ # :type - column type
46
+ # :notnull - integer flag for "NOT NULL"
47
+ # :dflt_value - default value
48
+ # :pk - integer flag for primary key
49
+ #
50
+ # Returns the new ColumnDefinition
51
+ def self.from_pragma(pragma)
52
+ type = INVERSE_TYPE_MAP[pragma[:type]]
53
+ options = {
54
+ :null => (pragma[:notnull] != 1),
55
+ :primary => (pragma[:pk] == 1),
56
+ :default => (pragma[:dflt_value])
57
+ }
58
+
59
+ if options[:default]
60
+ case type
61
+ when :integer
62
+ options[:default] = options[:default].to_i
63
+ when :float
64
+ options[:default] = options[:default].to_f
65
+ end
66
+ end
67
+
68
+ self.new(type, pragma[:name], options)
69
+ end
70
+
71
+ protected
72
+
73
+ def sql_type
74
+ if TYPE_MAP[@type]
75
+ TYPE_MAP[@type]
76
+ else
77
+ raise "Unrecognized column type: #{@type.inspect}"
78
+ end
79
+ end
80
+
81
+ def sql_options
82
+ sql_options = []
83
+
84
+ @options.each do |key, value|
85
+ case key
86
+ when :primary
87
+ if value
88
+ sql_options << "PRIMARY KEY ASC AUTOINCREMENT"
89
+ end
90
+ when :null
91
+ if !value
92
+ sql_options << "NOT NULL"
93
+ end
94
+ when :default
95
+ if value
96
+ sql_options << "DEFAULT #{value.inspect}"
97
+ end
98
+ else
99
+ raise "Unrecognized column option: #{key.inspect}"
100
+ end
101
+ end
102
+
103
+ if sql_options.any?
104
+ sql_options.join(" ")
105
+ else
106
+ nil
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,35 @@
1
+ module MotionRecord
2
+ module Schema
3
+ class IndexDefinition
4
+
5
+ # Initialize the index definition
6
+ #
7
+ # table_name - name of the table
8
+ # columns - either the String name of the column to index or an Array
9
+ # of column names
10
+ # options - optional Hash of options for the index
11
+ # :unique - set to true to create a unique index
12
+ # :name - provide a String to override the default index name
13
+ def initialize(table_name, columns, options={})
14
+ @table_name = table_name
15
+ @columns = columns.is_a?(Array) ? columns : [columns]
16
+
17
+ @name = options[:name] || build_name_from_columns
18
+ @unique = !!options[:unique]
19
+ end
20
+
21
+ # Add the index to the database
22
+ def execute
23
+ index_statement = "CREATE#{' UNIQUE' if @unique} INDEX #{@name} ON #{@table_name} (#{@columns.join ", "})"
24
+
25
+ MotionRecord::Base.connection.execute index_statement
26
+ end
27
+
28
+ protected
29
+
30
+ def build_name_from_columns
31
+ "index_#{@table_name}_on_#{@columns.join "_and_"}"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ # MotionRecord::Schema::Migration represents versions of migrations which have
2
+ # been run
3
+
4
+ module MotionRecord
5
+ module Schema
6
+ class Migration < Base
7
+ class << self
8
+ def table_name
9
+ "schema_migrations"
10
+ end
11
+
12
+ def table_exists?
13
+ connection.table_exists?(table_name)
14
+ end
15
+
16
+ def create_table
17
+ table = Schema::TableDefinition.new(table_name, id: false)
18
+ table.integer :version, :null => false
19
+ table.index :version, :unique => true
20
+ table.execute
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,32 @@
1
+ module MotionRecord
2
+ module Schema
3
+ class MigrationDefinition
4
+ attr_reader :version
5
+ attr_reader :name
6
+
7
+ def initialize(version, name = nil)
8
+ @version = version.to_i
9
+ @name = name || "Migration ##{@version}"
10
+ @definitions = []
11
+ end
12
+
13
+ def execute
14
+ @definitions.each(&:execute)
15
+ end
16
+
17
+ def create_table(name, options = {})
18
+ table_definition = TableDefinition.new(name, options)
19
+
20
+ if block_given?
21
+ yield table_definition
22
+ end
23
+
24
+ @definitions << table_definition
25
+ end
26
+
27
+ def add_index(name, columns, options = {})
28
+ @definitions << IndexDefinition.new(name, columns, options)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,39 @@
1
+ module MotionRecord
2
+ module Schema
3
+ class Migrator
4
+
5
+ attr_reader :migrations
6
+
7
+ def initialize(migrations)
8
+ @migrations = migrations
9
+ @migrated_versions = nil
10
+
11
+ initialize_schema_table
12
+ end
13
+
14
+ def run
15
+ pending_migrations.each do |migration|
16
+ migration.execute
17
+ @migrated_versions << migration.version
18
+ Schema::Migration.create!(version: migration.version)
19
+ end
20
+ end
21
+
22
+ def pending_migrations
23
+ @migrations.reject { |migration| migrated.include?(migration.version) }
24
+ end
25
+
26
+ def migrated
27
+ @migrated_versions ||= Schema::Migration.pluck(:version).sort
28
+ end
29
+
30
+ protected
31
+
32
+ def initialize_schema_table
33
+ unless Schema::Migration.table_exists?
34
+ Schema::Migration.create_table
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,18 @@
1
+ # DSL helper for defining migrations
2
+ module MotionRecord
3
+ module Schema
4
+ class MigratorDefinition
5
+ attr_reader :migrations
6
+
7
+ def initialize
8
+ @migrations = []
9
+ end
10
+
11
+ def migration(version, name=nil, &block)
12
+ migration_definition = Schema::MigrationDefinition.new(version, name)
13
+ migration_definition.instance_eval &block
14
+ @migrations << migration_definition
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,46 @@
1
+ module MotionRecord
2
+ module Schema
3
+ class TableDefinition
4
+ def initialize(name, options={})
5
+ @name = name
6
+ @columns = []
7
+ @index_definitions = []
8
+
9
+ unless options.has_key?(:id) && !options[:id]
10
+ add_default_primary_column
11
+ end
12
+ end
13
+
14
+ def execute
15
+ # Create table
16
+ column_sql = @columns.map(&:to_sql_definition).join(", ")
17
+ MotionRecord::Base.connection.execute "CREATE TABLE #{@name} (#{column_sql})"
18
+
19
+ # Create table's indexes
20
+ @index_definitions.each(&:execute)
21
+ end
22
+
23
+ def text(column_name, options={})
24
+ @columns << ColumnDefinition.new(:text, column_name, options)
25
+ end
26
+
27
+ def integer(column_name, options={})
28
+ @columns << ColumnDefinition.new(:integer, column_name, options)
29
+ end
30
+
31
+ def float(column_name, options={})
32
+ @columns << ColumnDefinition.new(:float, column_name, options)
33
+ end
34
+
35
+ def index(columns, options={})
36
+ @index_definitions << IndexDefinition.new(@name, columns, options)
37
+ end
38
+
39
+ protected
40
+
41
+ def add_default_primary_column
42
+ @columns << ColumnDefinition.new(:integer, "id", primary: true)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,35 @@
1
+ module MotionRecord
2
+ module Schema
3
+ # Define and run all pending migrations (should be done during app setup)
4
+ #
5
+ # options - Hash of configuration options for the SQLite connection
6
+ # :file - full name of the database file, or :memory for in-memory
7
+ # database files (default is "app.sqlite3" in the app's
8
+ # `/Library/Application Support` folder)
9
+ # :debug - set to false to turn off SQL debug logging
10
+ #
11
+ # Example:
12
+ #
13
+ # MotionRecord::Schema.up! do
14
+ # migration 1, "Create events table" do
15
+ # create_table "events" do |t|
16
+ # t.text :name, :null => false
17
+ # t.text :properties
18
+ # end
19
+ # end
20
+ #
21
+ # migration 2, "Index events table" do
22
+ # add_index "events", "name", :unique => true
23
+ # end
24
+ # end
25
+ #
26
+ def self.up!(options={}, &block)
27
+ ConnectionAdapters::SQLiteAdapter.configure(options)
28
+
29
+ definition = Schema::MigratorDefinition.new
30
+ definition.instance_eval &block
31
+
32
+ Schema::Migrator.new(definition.migrations).run
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,129 @@
1
+ # A model for building scoped database queries
2
+ #
3
+
4
+ module MotionRecord
5
+ class Scope
6
+
7
+ attr_reader :klass
8
+ attr_reader :conditions
9
+
10
+ def initialize(klass, options = {})
11
+ @klass = klass
12
+ @conditions = options[:conditions] || {} # TODO: freeze?
13
+ @order = options[:order]
14
+ @limit = options[:limit]
15
+ end
16
+
17
+ # Scope builder
18
+
19
+ def where(conditions={})
20
+ Scope.new(@klass, :conditions => @conditions.merge(conditions), :order => @order, :limit => @limit)
21
+ end
22
+
23
+ def order(ordering_term)
24
+ Scope.new(@klass, :conditions => @conditions, :order => ordering_term, :limit => @limit)
25
+ end
26
+
27
+ def limit(limit_value)
28
+ Scope.new(@klass, :conditions => @conditions, :order => @order, :limit => limit_value)
29
+ end
30
+
31
+ # Read-only queries
32
+
33
+ def exists?
34
+ count > 0
35
+ end
36
+
37
+ def first
38
+ limit(1).find_all.first
39
+ end
40
+
41
+ def find(id)
42
+ self.where(@klass.primary_key => id).first
43
+ end
44
+
45
+ def find_all
46
+ connection.select(self).map { |row| @klass.from_table_params(row) }
47
+ end
48
+
49
+ def pluck(attribute)
50
+ connection.select(self).map { |row| row[attribute] }
51
+ end
52
+
53
+ # Persistence queries
54
+
55
+ def update_all(params)
56
+ connection.update(self, params)
57
+ end
58
+
59
+ def delete_all
60
+ connection.delete(self)
61
+ end
62
+
63
+ # Calculations
64
+
65
+ def count(column=nil)
66
+ calculate(:count, column)
67
+ end
68
+
69
+ def maximum(column)
70
+ calculate(:maximum, column)
71
+ end
72
+
73
+ def minimum(column)
74
+ calculate(:minimum, column)
75
+ end
76
+
77
+ def sum(column)
78
+ calculate(:sum, column)
79
+ end
80
+
81
+ def average(column)
82
+ calculate(:average, column)
83
+ end
84
+
85
+ # SQL helpers
86
+
87
+ def predicate?
88
+ predicate_segments.any?
89
+ end
90
+
91
+ def predicate
92
+ predicate_segments.join(" ")
93
+ end
94
+
95
+ def predicate_values
96
+ condition_columns.map { |column| @conditions[column] }
97
+ end
98
+
99
+ protected
100
+
101
+ def calculate(method, column)
102
+ connection.calculate(self, method, column)
103
+ end
104
+
105
+ def predicate_segments
106
+ unless @predicate_segments
107
+ @predicate_segments = []
108
+ if condition_columns.any?
109
+ @predicate_segments << "WHERE #{condition_columns.map { |c| "#{c} = ? " }.join " AND "}"
110
+ end
111
+ if @order
112
+ @predicate_segments << "ORDER BY #{@order}"
113
+ end
114
+ if @limit
115
+ @predicate_segments << "LIMIT #{@limit}"
116
+ end
117
+ end
118
+ @predicate_segments
119
+ end
120
+
121
+ def condition_columns
122
+ @condition_columns ||= @conditions.keys
123
+ end
124
+
125
+ def connection
126
+ Base.connection
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,89 @@
1
+ # These helper methods make it possible to call scope methods like `where` and
2
+ # `find` directly on MotionRecord::Base classes
3
+ #
4
+ # Example:
5
+ #
6
+ # Event.where(:name => "launched").find_all
7
+ #
8
+ # FIXME: MotionRecord::Persistence includes this module, but the normal file
9
+ # ordering breaks the dependency
10
+ module MotionRecord
11
+ module ScopeHelpers
12
+ module ClassMethods
13
+
14
+ # Read-only queries
15
+
16
+ def exists?
17
+ scoped.exists?
18
+ end
19
+
20
+ def first
21
+ scoped.first
22
+ end
23
+
24
+ def find(id)
25
+ scoped.find(id)
26
+ end
27
+
28
+ def find_all
29
+ scoped.find_all
30
+ end
31
+
32
+ def pluck(attribute)
33
+ scoped.pluck(attribute)
34
+ end
35
+
36
+ # Persistence queries
37
+
38
+ def update_all(params)
39
+ scoped.update_all(params)
40
+ end
41
+
42
+ def delete_all
43
+ scoped.delete_all
44
+ end
45
+
46
+ # Calculations
47
+
48
+ def count(column=nil)
49
+ scoped.count(column)
50
+ end
51
+
52
+ def maximum(column)
53
+ scoped.maximum(column)
54
+ end
55
+
56
+ def minimum(column)
57
+ scoped.minimum(column)
58
+ end
59
+
60
+ def sum(column)
61
+ scoped.sum(column)
62
+ end
63
+
64
+ def average(column)
65
+ scoped.average(column)
66
+ end
67
+
68
+ # Scope building
69
+
70
+ def where(conditions={})
71
+ scoped.where(conditions)
72
+ end
73
+
74
+ def order(ordering_term)
75
+ scoped.order(ordering_term)
76
+ end
77
+
78
+ def limit(limit_value)
79
+ scoped.limit(limit_value)
80
+ end
81
+
82
+ protected
83
+
84
+ def scoped
85
+ Scope.new(self)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,20 @@
1
+ module MotionRecord
2
+ module Serialization
3
+ class BaseSerializer
4
+ # column - a Schema::ColumnDefinition object
5
+ def initialize(column)
6
+ @column = column
7
+ end
8
+
9
+ # Override this method in a subclass to define the custom serializer
10
+ def serialize(value)
11
+ raise "Must be implemented"
12
+ end
13
+
14
+ # Override this method in a subclass to define the custom serializer
15
+ def deserialize(value)
16
+ raise "Must be implemented"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,26 @@
1
+ module MotionRecord
2
+ module Serialization
3
+ class BooleanSerializer < BaseSerializer
4
+
5
+ def serialize(value)
6
+ if @column.type == :integer
7
+ value ? 1 : 0
8
+ else
9
+ raise "Can't serialize #{value.inspect} to #{@column.type.inspect}"
10
+ end
11
+ end
12
+
13
+ def deserialize(value)
14
+ if @column.type == :integer
15
+ if value == 0 || value.nil?
16
+ false
17
+ else
18
+ true
19
+ end
20
+ else
21
+ raise "Can't deserialize #{value.inspect} from #{@column.type.inspect}"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,14 @@
1
+ # This serializer converts the object back and forth as-is
2
+ module MotionRecord
3
+ module Serialization
4
+ class DefaultSerializer < BaseSerializer
5
+ def serialize(value)
6
+ value
7
+ end
8
+
9
+ def deserialize(value)
10
+ value
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,38 @@
1
+ module MotionRecord
2
+ module Serialization
3
+ class JSONParserError < StandardError; end
4
+
5
+ class JSONSerializer < BaseSerializer
6
+
7
+ def serialize(value)
8
+ unless @column.type == :text
9
+ raise "JSON can only be serialized to TEXT columns"
10
+ end
11
+ self.class.generate_json(value)
12
+ end
13
+
14
+ def deserialize(value)
15
+ unless @column.type == :text
16
+ raise "JSON can only be deserialized from TEXT columns"
17
+ end
18
+ self.class.parse_json(value)
19
+ end
20
+
21
+ # JSON generate/parse code is hoisted from BubbleWrap::JSON
22
+
23
+ def self.generate_json(obj)
24
+ NSJSONSerialization.dataWithJSONObject(obj, options:0, error:nil).to_str
25
+ end
26
+
27
+ def self.parse_json(str_data)
28
+ return nil unless str_data
29
+ data = str_data.respond_to?('dataUsingEncoding:') ? str_data.dataUsingEncoding(NSUTF8StringEncoding) : str_data
30
+ opts = NSJSONReadingMutableContainers | NSJSONReadingMutableLeaves | NSJSONReadingAllowFragments
31
+ error = Pointer.new(:id)
32
+ obj = NSJSONSerialization.JSONObjectWithData(data, options:opts, error:error)
33
+ raise JSONParserError, error[0].description if error[0]
34
+ obj
35
+ end
36
+ end
37
+ end
38
+ end