squirm_model 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. data/.gitignore +19 -0
  2. data/.yardopts +4 -0
  3. data/Gemfile +8 -0
  4. data/Rakefile +18 -0
  5. data/bin/squirm +33 -0
  6. data/lib/squirm/migrator.rb +14 -0
  7. data/lib/squirm/migrator/ambry.yml +124 -0
  8. data/lib/squirm/migrator/column.rb +59 -0
  9. data/lib/squirm/migrator/table.rb +49 -0
  10. data/lib/squirm/migrator/template.rb +46 -0
  11. data/lib/squirm/migrator/templates/sql/api/create.sql.erb +20 -0
  12. data/lib/squirm/migrator/templates/sql/api/delete.sql.erb +10 -0
  13. data/lib/squirm/migrator/templates/sql/api/get.sql.erb +5 -0
  14. data/lib/squirm/migrator/templates/sql/api/update.sql.erb +14 -0
  15. data/lib/squirm/migrator/templates/sql/columns/created.sql.erb +12 -0
  16. data/lib/squirm/migrator/templates/sql/columns/foreign_key.sql.erb +7 -0
  17. data/lib/squirm/migrator/templates/sql/columns/updated.sql.erb +12 -0
  18. data/lib/squirm/migrator/templates/sql/index.sql.erb +2 -0
  19. data/lib/squirm/migrator/templates/sql/layout.sql.erb +5 -0
  20. data/lib/squirm/migrator/templates/sql/queries/procedure_info.sql +14 -0
  21. data/lib/squirm/migrator/templates/sql/queries/table_info.sql +23 -0
  22. data/lib/squirm/migrator/templates/sql/schema.sql.erb +3 -0
  23. data/lib/squirm/migrator/templates/sql/table.sql.erb +14 -0
  24. data/lib/squirm/model.rb +134 -0
  25. data/lib/squirm/model/method_definer.rb +54 -0
  26. data/lib/squirm/model/sample.rb +12 -0
  27. data/lib/squirm/model/schema_functions.sql +14 -0
  28. data/lib/squirm/model/version.rb +5 -0
  29. data/squirm_model.gemspec +26 -0
  30. data/test/helper.rb +42 -0
  31. data/test/model_test.rb +66 -0
  32. metadata +134 -0
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ spec/coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+ Gemfile.lock
15
+
16
+ # YARD artifacts
17
+ .yardoc
18
+ _yardoc
19
+ doc/
@@ -0,0 +1,4 @@
1
+ --files=*.md
2
+ --protected
3
+ --list-undoc
4
+ --exclude lib/squirm/version
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source :rubygems
2
+
3
+ gem "activemodel"
4
+ gem "squirm", :path => "../squirm"
5
+ gem "minitest"
6
+ gem "thor"
7
+
8
+ gemspec
@@ -0,0 +1,18 @@
1
+ require 'rake/clean'
2
+ require 'rake/testtask'
3
+
4
+ CLEAN.include "pkg", "test/coverage", "doc", "*.gem"
5
+
6
+ task default: :test
7
+
8
+ task :gem do
9
+ sh "gem build squirm_model.gemspec"
10
+ end
11
+
12
+ task :test do
13
+ Rake::TestTask.new do |t|
14
+ t.libs << "test"
15
+ t.test_files = FileList["test/*_test.rb"]
16
+ t.verbose = false
17
+ end
18
+ end
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Leave this for now for dev purposes.
4
+ require "bundler/setup"
5
+
6
+ require "squirm/migrator"
7
+ require "thor"
8
+
9
+ module Squirm
10
+
11
+ module Migrator
12
+
13
+ class CLI < Thor
14
+
15
+ desc "table [name] [column list]", "Outputs a table definition"
16
+ method_option :api, :aliases => "-a", :desc => "Include Squirm::Model API"
17
+ def table(name, *columns)
18
+ layout = Template.from_path("sql/layout.sql.erb")
19
+ table = Table.new(name).add_column(*columns)
20
+ output = layout.render do
21
+ buffer = table.template.render
22
+ if options[:api]
23
+ buffer << table.api.map(&:render).join
24
+ end
25
+ buffer
26
+ end
27
+ print output
28
+ end
29
+ end
30
+ end
31
+ end
32
+ Squirm::Migrator::CLI.start
33
+
@@ -0,0 +1,14 @@
1
+ require "squirm"
2
+ require "ambry"
3
+ require "ambry/adapters/yaml"
4
+
5
+ module Squirm
6
+ module Migrator
7
+ yaml = File.expand_path("../migrator/ambry.yml", __FILE__)
8
+ Ambry::Adapters::YAML.new file: yaml, name: :squirm
9
+ end
10
+ end
11
+
12
+ require "squirm/migrator/column"
13
+ require "squirm/migrator/table"
14
+ require "squirm/migrator/template"
@@ -0,0 +1,124 @@
1
+ ---
2
+ "Squirm::Migrator::Column::Type":
3
+ :serial:
4
+ :definition: "SERIAL NOT NULL PRIMARY KEY"
5
+ :priority: 10
6
+ :creatable: false
7
+ :updatable: false
8
+ :findable: false
9
+ :sql: integer
10
+ :patterns:
11
+ - !ruby/regexp /\Aid\z/
12
+ :user_name:
13
+ :definition: "VARCHAR(32) NOT NULL UNIQUE"
14
+ :priority: 10
15
+ :creatable: true
16
+ :updatable: true
17
+ :findable: true
18
+ :sql: text
19
+ :patterns:
20
+ - !ruby/regexp /\A(user_name|login)\z/
21
+ :int:
22
+ :definition: "INTEGER NOT NULL"
23
+ :priority: 10
24
+ :creatable: true
25
+ :updatable: true
26
+ :findable: false
27
+ :sql: integer
28
+ :patterns:
29
+ - !ruby/regexp /_count\z/
30
+ :timestamp:
31
+ :definition: "TIMESTAMP WITH TIME ZONE"
32
+ :priority: 9
33
+ :creatable: true
34
+ :updatable: true
35
+ :findable: false
36
+ :sql: text
37
+ :patterns:
38
+ - !ruby/regexp /_(at|time|on)\z/
39
+ :created:
40
+ :definition: "TIMESTAMP WITH TIME ZONE NOT NULL"
41
+ :priority: 10
42
+ :templates: ["sql/columns/created"]
43
+ :creatable: false
44
+ :updatable: false
45
+ :findable: false
46
+ :sql: text
47
+ :patterns:
48
+ - !ruby/regexp /creat(ed|ion)_(at|on|time)\z/
49
+ - !ruby/regexp /\Atime_created\z/
50
+ :email:
51
+ :definition: "VARCHAR(64) NOT NULL UNIQUE"
52
+ :priority: 10
53
+ :creatable: true
54
+ :updatable: true
55
+ :findable: true
56
+ :sql: text
57
+ :patterns:
58
+ - !ruby/regexp /email\z/
59
+ :geo:
60
+ :definition: "NUMERIC(18,12)"
61
+ :priority: 10
62
+ :creatable: true
63
+ :updatable: true
64
+ :findable: false
65
+ :sql: numeric
66
+ :patterns:
67
+ - !ruby/regexp /_?(lat|lng|lon|latitude|longitude)\z/
68
+ :default:
69
+ :definition: "VARCHAR(256)"
70
+ :priority: 0
71
+ :creatable: true
72
+ :updatable: true
73
+ :findable: false
74
+ :sql: text
75
+ :patterns:
76
+ - !ruby/regexp /.*/
77
+ :text:
78
+ :definition: "TEXT"
79
+ :priority: 10
80
+ :creatable: true
81
+ :updatable: true
82
+ :findable: false
83
+ :sql: text
84
+ :patterns:
85
+ - !ruby/regexp /_?(body|text|json|html|xml|description|bio|profile)/
86
+ :boolean:
87
+ :definition: "BOOLEAN NOT NULL"
88
+ :priority: 10
89
+ :creatable: true
90
+ :updatable: true
91
+ :findable: false
92
+ :sql: bool
93
+ :patterns:
94
+ - !ruby/regexp /(is|has)_.*/
95
+ :date:
96
+ :definition: "DATE"
97
+ :priority: 10
98
+ :creatable: true
99
+ :updatable: true
100
+ :findable: false
101
+ :sql: text
102
+ :patterns:
103
+ - !ruby/regexp /_date\z/
104
+ :updated:
105
+ :definition: "TIMESTAMP WITH TIME ZONE NOT NULL"
106
+ :priority: 10
107
+ :creatable: false
108
+ :updatable: false
109
+ :findable: false
110
+ :templates: ["sql/columns/updated"]
111
+ :sql: text
112
+ :patterns:
113
+ - !ruby/regexp /updated?_(at|on|time)\z/
114
+ - !ruby/regexp /\Atime_updated\z/
115
+ :foreign_key:
116
+ :definition: "INTEGER NOT NULL"
117
+ :priority: 10
118
+ :templates: ["sql/index", "sql/columns/foreign_key"]
119
+ :creatable: true
120
+ :updatable: true
121
+ :findable: true
122
+ :sql: integer
123
+ :patterns:
124
+ - !ruby/regexp /_id\z/
@@ -0,0 +1,59 @@
1
+ module Squirm
2
+ module Migrator
3
+ class Column
4
+
5
+ class Type
6
+ extend Ambry::Model
7
+ use :squirm
8
+ field :name, :definition, :patterns, :priority, :templates, :findable,
9
+ :creatable, :updatable, :sql
10
+
11
+ undef :templates
12
+ def templates
13
+ @attributes[:templates] or []
14
+ end
15
+
16
+ filters do
17
+ def by_priority
18
+ sort {|a, b| b.priority <=> a.priority}
19
+ end
20
+ end
21
+
22
+ def =~(string)
23
+ patterns.any? {|pattern| string.match pattern}
24
+ end
25
+
26
+ end
27
+
28
+ attr :name
29
+
30
+ def initialize(name)
31
+ @name = name
32
+ end
33
+
34
+ def quoted_name
35
+ Squirm.quote_ident name
36
+ end
37
+
38
+ def type
39
+ @type ||= Type.by_priority.first {|type| type =~ name}
40
+ end
41
+
42
+ def definition
43
+ type.definition
44
+ end
45
+
46
+ def to_sql
47
+ "#{quoted_name} #{definition}"
48
+ end
49
+
50
+ def templates
51
+ @templates ||= begin
52
+ @templates = type.templates.map do |template|
53
+ Template.from_path "#{template}.sql.erb", :column => self
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,49 @@
1
+ module Squirm
2
+
3
+ module Migrator
4
+
5
+ class Table
6
+
7
+ attr :columns, :name
8
+
9
+ def initialize(name)
10
+ @name = name
11
+ @columns = []
12
+ end
13
+
14
+ def quoted_name
15
+ Squirm.quote_ident name
16
+ end
17
+
18
+ def add_column(*names)
19
+ names.each do |name|
20
+ column = Column.new(name)
21
+ column.templates.map {|t| t.table = self}
22
+ @columns << column
23
+ end
24
+ self
25
+ end
26
+
27
+ def api
28
+ templates = ["schema", "api/get", "api/create", "api/update", "api/delete"]
29
+ templates.map do |template|
30
+ erb = File.read File.expand_path("../templates/sql/#{template}.sql.erb", __FILE__)
31
+ Template.new erb, :table => self
32
+ end
33
+ end
34
+
35
+ def template
36
+ erb = File.read File.expand_path("../templates/sql/table.sql.erb", __FILE__)
37
+ Template.new erb, :table => self, :column_padding => column_padding
38
+ end
39
+
40
+ private
41
+
42
+ def column_padding
43
+ columns.inject(0) do |longest, col|
44
+ col.name.length > longest ? col.name.length : longest
45
+ end + 3
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,46 @@
1
+ require "erb"
2
+ require "pathname"
3
+
4
+ module Squirm
5
+
6
+ module Migrator
7
+
8
+ # A class to easily create a binding for an Erb template, because I don't like
9
+ # the def_class and def_method methods that ship with Erb. Not using an
10
+ # OpenStruct either because it uses an internal @table variable that makes it
11
+ # hard to deal with when our main template has a variable named "table."
12
+ class Template
13
+ attr :__template__
14
+
15
+ def initialize(template, hash = {})
16
+ hash.each {|key, value| self[key] = value}
17
+ @__template__ = template
18
+ end
19
+
20
+ def self.from_path(path, hash = {})
21
+ unless Pathname.new(path).absolute?
22
+ path = File.expand_path(File.join("..", "templates", path), __FILE__)
23
+ end
24
+ new File.read(path), hash
25
+ end
26
+
27
+ def render(&block)
28
+ ERB.new(__template__, nil, '-%>').result(binding)
29
+ end
30
+
31
+ private
32
+
33
+ def method_missing(sym, *args, &block)
34
+ sym =~ /=\z/ ? self[sym.to_s.gsub(/=\z/, '')] = args.first : self[sym]
35
+ end
36
+
37
+ def []=(key, value)
38
+ instance_variable_set :"@#{key}", value
39
+ end
40
+
41
+ def [](key)
42
+ instance_variable_get :"@#{key}"
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,20 @@
1
+ <% cols = table.columns.select {|col| col.type.creatable}.sort_by(&:name) %>
2
+
3
+ CREATE FUNCTION <%= Squirm.quote_ident(table.name) %>."create"(
4
+ <%= cols.map {|c| "_#{c.name} #{c.type.sql}" }.join(",\n ") %>
5
+ ) RETURNS integer AS $$
6
+ DECLARE
7
+ new_id integer;
8
+ BEGIN
9
+ INSERT INTO <%= Squirm.quote_ident(table.name) %> (
10
+ <%= cols.map(&:name).join(",\n ") %>
11
+ )
12
+ VALUES (
13
+ <%= cols.map {|c| "_#{c.name}"}.join(",\n ") %>
14
+ )
15
+ RETURNING id INTO new_id;
16
+ IF FOUND THEN
17
+ RETURN new_id;
18
+ END IF;
19
+ END;
20
+ $$ LANGUAGE 'plpgsql';
@@ -0,0 +1,10 @@
1
+ <% table_name = Squirm.quote_ident(table.name) %>
2
+
3
+ CREATE FUNCTION <%= table_name %>."delete"(_id integer) RETURNS void AS $$
4
+ BEGIN
5
+ DELETE FROM <%= table_name %> WHERE id = _id;
6
+ IF NOT FOUND THEN
7
+ RAISE EXCEPTION 'no such record in table <%= table_name %>: %', _id;
8
+ END IF;
9
+ END;
10
+ $$ LANGUAGE 'plpgsql';
@@ -0,0 +1,5 @@
1
+ CREATE FUNCTION <%= Squirm.quote_ident(table.name) %>."get"(_id integer) RETURNS SETOF <%= Squirm.quote_ident(table.name) %> AS $$
2
+ BEGIN
3
+ RETURN QUERY SELECT * FROM <%= Squirm.quote_ident(table.name) %> WHERE id = _id;
4
+ END;
5
+ $$ LANGUAGE 'plpgsql' STABLE;
@@ -0,0 +1,14 @@
1
+ <% cols = table.columns.select {|col| col.type.updatable}.sort_by(&:name) %>
2
+ <% table_name = Squirm.quote_ident(table.name) %>
3
+
4
+ CREATE FUNCTION <%= table_name %>."update"(
5
+ _id integer,
6
+ <%= cols.map {|c| "_#{c.name} #{c.type.sql}" }.join(",\n ") %>
7
+ ) RETURNS void AS $$
8
+ BEGIN
9
+ UPDATE <%= table_name %> SET
10
+ <%= cols.map {|c| "#{c.name} = _#{c.name}"}.join(",\n ") %>
11
+ WHERE id = _id;
12
+ RETURN;
13
+ END;
14
+ $$ LANGUAGE 'plpgsql';
@@ -0,0 +1,12 @@
1
+ <% function_name = Squirm.quote_ident("update_#{table.name}_#{column.name}_timestamp") -%>
2
+ CREATE OR REPLACE FUNCTION <%= function_name %>()
3
+ RETURNS TRIGGER AS $$
4
+ BEGIN
5
+ NEW.<%= column.name %> = NOW();
6
+ RETURN NEW;
7
+ END;
8
+ $$ LANGUAGE 'plpgsql';
9
+
10
+ CREATE TRIGGER <%= function_name %>
11
+ BEFORE INSERT ON <%= table.quoted_name %>
12
+ FOR EACH ROW EXECUTE PROCEDURE <%= function_name %>();
@@ -0,0 +1,7 @@
1
+ /*
2
+ ALTER TABLE <%= table.quoted_name %> ADD CONSTRAINT <%= Squirm.quote_ident(column.name + "_fkey") %>
3
+ FOREIGN KEY (<%= column.quoted_name %>)
4
+ REFERENCES <%= Squirm.quote_ident(column.name.split("_")[0]) %> (<%= Squirm.quote_ident(column.name.split("_")[1]) %>)
5
+ ON DELETE RESTRICT
6
+ INITIALLY DEFERRED;
7
+ */
@@ -0,0 +1,12 @@
1
+ <% function_name = Squirm.quote_ident("update_#{table.name}_#{column.name}_timestamp") -%>
2
+ CREATE OR REPLACE FUNCTION <%= table.quoted_name %>.<%= function_name %>()
3
+ RETURNS TRIGGER AS $$
4
+ BEGIN
5
+ NEW.<%= column.name %> = NOW();
6
+ RETURN NEW;
7
+ END;
8
+ $$ LANGUAGE 'plpgsql';
9
+
10
+ CREATE TRIGGER <%= function_name %>
11
+ BEFORE INSERT OR UPDATE ON <%= table.quoted_name %>
12
+ FOR EACH ROW EXECUTE PROCEDURE <%= table.quoted_name %>.<%= function_name %>();
@@ -0,0 +1,2 @@
1
+ <% index_name = Squirm.quote_ident([table.name, column.name, "index"].join("_")) %>
2
+ CREATE INDEX <%= index_name %> ON <%= table.quoted_name %> (<%= column.quoted_name %>);
@@ -0,0 +1,5 @@
1
+ BEGIN;
2
+
3
+ <%= yield %>
4
+
5
+ ROLLBACK;
@@ -0,0 +1,14 @@
1
+ SELECT p.proname as "name",
2
+ pg_catalog.pg_get_function_result(p.oid) as "return_type",
3
+ pg_catalog.pg_get_function_arguments(p.oid) as "arguments",
4
+ CASE
5
+ WHEN p.proisagg THEN 'agg'
6
+ WHEN p.proiswindow THEN 'window'
7
+ WHEN p.prorettype = 'pg_catalog.trigger'::pg_catalog.regtype THEN 'trigger'
8
+ ELSE 'normal'
9
+ END as "procedure_type"
10
+ FROM pg_catalog.pg_proc p
11
+ LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
12
+ WHERE p.proname = $1::text
13
+ AND n.nspname = $2::text
14
+ LIMIT 1
@@ -0,0 +1,23 @@
1
+ SELECT
2
+ a.attname "name",
3
+ a.attnum "number",
4
+ t.typname "type",
5
+ a.attlen "length",
6
+ a.attnotnull "not_null",
7
+ a.atthasdef "has_default",
8
+ a.attndims "array_dimensions"
9
+
10
+ FROM pg_class AS c,
11
+ pg_attribute AS a,
12
+ pg_type AS t,
13
+ pg_namespace AS n
14
+
15
+ WHERE a.attnum > 0
16
+ AND a.attrelid = c.oid
17
+ AND c.relname = $1::text
18
+ AND c.relnamespace = n.oid
19
+ AND n.nspname = $2::text
20
+ AND a.atttypid = t.oid
21
+ AND a.attisdropped = 'f'
22
+
23
+ ORDER BY a.attnum
@@ -0,0 +1,3 @@
1
+ DROP SCHEMA IF EXISTS <%= table.quoted_name %> CASCADE;
2
+ CREATE SCHEMA <%= table.quoted_name %>;
3
+
@@ -0,0 +1,14 @@
1
+ DROP TABLE IF EXISTS <%= table.quoted_name %> CASCADE;
2
+
3
+ CREATE TABLE <%= table.quoted_name -%> (
4
+ <% table.columns.map do |column| -%>
5
+ <% comma = column == table.columns.last ? "" : "," -%>
6
+ <%= "%-#{column_padding}s%s%s" % [column.quoted_name, column.definition, comma] %>
7
+ <% end -%>
8
+ );
9
+
10
+ <% table.columns.each do |column| -%>
11
+ <% column.templates.each do |template| -%>
12
+ <%= template.render %>
13
+ <% end -%>
14
+ <% end -%>
@@ -0,0 +1,134 @@
1
+ require "active_model"
2
+ require "squirm/model/sample"
3
+ require "squirm/model/method_definer"
4
+ require "squirm/migrator"
5
+
6
+ module Squirm
7
+
8
+ module Model
9
+
10
+ include ActiveModel::Naming
11
+
12
+ # Raised when an instance could not be found.
13
+ class NotFound < StandardError
14
+ end
15
+
16
+ attr :api
17
+ attr :__attributes
18
+
19
+ def self.extended(base)
20
+ base.class_eval do
21
+ include ActiveModel::Conversion
22
+ include InstanceMethods
23
+ end
24
+ end
25
+
26
+ # Invoke {#finalize} on all Squirm models.
27
+ def self.finalize
28
+ ObjectSpace.each_object(self) do |mod|
29
+ mod.finalize
30
+ end
31
+ end
32
+
33
+ # Invoke to load API. This allows classes to be set up before the API
34
+ # has been created, or the db connection established.
35
+ def finalize
36
+ return if defined? @api
37
+ sql = Pathname(__FILE__).dirname.join("model/schema_functions.sql").read
38
+ schema_name = model_name.to_s.downcase
39
+ Squirm.exec(sql, [schema_name]) do |result|
40
+ @api = OpenStruct.new Hash[result.map do |row| [
41
+ row["name"].to_sym,
42
+ Procedure.new(row["name"], :schema => schema_name).load
43
+ ]
44
+ end]
45
+ end
46
+ end
47
+
48
+ def sample(arg = nil, &block)
49
+ if block_given?
50
+ @sample_block = block
51
+ yield MethodDefiner.new(self)
52
+ elsif !arg
53
+ @sample ||= begin
54
+ @sample = Sample.new
55
+ @sample_block.call(@sample)
56
+ @sample
57
+ end
58
+ else
59
+ sample = Sample.new
60
+ @sample_block.call(sample)
61
+ sample.each do |key, value|
62
+ setter = :"#{key}="
63
+ arg.send(setter, value) if arg.respond_to?(setter)
64
+ end
65
+ return arg
66
+ end
67
+ end
68
+
69
+ def to_ddl
70
+ buffer = []
71
+ table = Squirm::Migrator::Table.new(name.downcase)
72
+ table.add_column(*sample.keys)
73
+ buffer << table.template.render
74
+ table.api.each {|template| buffer << template.render}
75
+ buffer.map(&:strip).join("\n\n")
76
+ end
77
+
78
+ def create(*args)
79
+ get api.create[*args]
80
+ end
81
+
82
+ def get(id)
83
+ api.get.call(id) do |result|
84
+ raise NotFound if result.ntuples == 0
85
+ hash = result.first
86
+ instance = allocate
87
+ instance.instance_variable_set :@__attributes, hash
88
+ instance
89
+ end
90
+ end
91
+
92
+ def delete(id)
93
+ api.delete[id]
94
+ end
95
+
96
+ module InstanceMethods
97
+
98
+ def initialize(options = {})
99
+ @__attributes = options
100
+ end
101
+
102
+ def update(params)
103
+ procedure = self.class.api.update
104
+ procedure.call to_hash.merge params
105
+ end
106
+
107
+ def to_hash
108
+ Hash[self.class.sample.keys.map {|key| [key, send(key)]}]
109
+ end
110
+
111
+ def save
112
+ if persisted?
113
+ update to_hash
114
+ else
115
+ @id = self.class.api.create[to_hash]
116
+ reload
117
+ end
118
+ end
119
+
120
+ def persisted?
121
+ !! self.id
122
+ end
123
+
124
+ def reload
125
+ self.class.api.get.call(id) do |result|
126
+ @__attributes = result.first
127
+ @__attributes.keys.each do |key|
128
+ remove_instance_variable :"@#{key}"
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,54 @@
1
+ require "date"
2
+
3
+ module Squirm
4
+ module Model
5
+ class MethodDefiner
6
+ def initialize(model_class)
7
+ @model_class = model_class
8
+ end
9
+
10
+ def define_getter(attribute, code)
11
+ @model_class.class_eval(<<-EOD, __FILE__, __LINE__ + 1)
12
+ def #{attribute}
13
+ if defined? @#{attribute}
14
+ @#{attribute}
15
+ else
16
+ return unless @__attributes.key?("#{attribute}")
17
+ #{code % attribute}
18
+ end
19
+ end
20
+ EOD
21
+ end
22
+
23
+ def define_setter(attribute)
24
+ @model_class.class_eval(<<-EOD, __FILE__, __LINE__ + 1)
25
+ def #{attribute}=(value)
26
+ @#{attribute} = value
27
+ end
28
+ EOD
29
+ end
30
+
31
+ def method_missing(sym, *args)
32
+ attribute = sym.to_s.tr("=", "")
33
+ value = args.first
34
+ base = '@__attributes["%s"]'
35
+ cast = begin
36
+ case value
37
+ when Float then base + '.to_f'
38
+ when Rational then base + '.to_r'
39
+ when Complex then base + '.to_c'
40
+ when Symbol then base + '.to_sym'
41
+ when Numeric then base + '.to_i'
42
+ when TrueClass then base + ' == "t"'
43
+ when FalseClass then base + ' == "t"'
44
+ when DateTime then "DateTime.parse(#{base})"
45
+ when Date then "Date.parse(#{base})"
46
+ else base + ".to_s"
47
+ end
48
+ end
49
+ define_getter(attribute, cast)
50
+ define_setter(attribute) unless value.frozen?
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,12 @@
1
+ require "ostruct"
2
+ require "forwardable"
3
+
4
+ module Squirm
5
+ module Model
6
+ class Sample < OpenStruct
7
+ extend Forwardable
8
+ include Enumerable
9
+ def_delegators :@table, :each, :keys, :[], :[]=, :to_hash
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ SELECT
2
+ p.proname as "name",
3
+ pg_catalog.pg_get_function_result(p.oid) as "return_type",
4
+ pg_catalog.pg_get_function_arguments(p.oid) as "arguments",
5
+ CASE
6
+ WHEN p.proisagg THEN 'agg'
7
+ WHEN p.proiswindow THEN 'window'
8
+ WHEN p.prorettype = 'pg_catalog.trigger'::pg_catalog.regtype THEN 'trigger'
9
+ ELSE 'normal'
10
+ END as "type"
11
+ FROM pg_catalog.pg_proc p
12
+ LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
13
+ WHERE n.nspname = $1
14
+
@@ -0,0 +1,5 @@
1
+ module Squirm
2
+ module Model
3
+ VERSION = "0.0.0"
4
+ end
5
+ end
@@ -0,0 +1,26 @@
1
+ require File.expand_path("../lib/squirm/model/version", __FILE__)
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "squirm_model"
5
+ s.version = Squirm::Model::VERSION
6
+ s.platform = Gem::Platform::RUBY
7
+ s.authors = ["Norman Clarke"]
8
+ s.email = ["norman@njclarke.com"]
9
+ s.homepage = "http://github.com/bvision/squirm_model"
10
+ s.summary = %q{"An anti-ORM for database-loving programmers"}
11
+ s.description = %q{"Squirm is an anti-ORM for database-loving programmers"}
12
+ s.bindir = "bin"
13
+ s.files = `git ls-files`.split("\n")
14
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
+ s.require_paths = ["lib"]
17
+ s.required_ruby_version = ">= 1.9"
18
+
19
+ s.add_development_dependency "minitest", ">= 2.6"
20
+ s.add_runtime_dependency "squirm", "> 0.0.0"
21
+ s.add_runtime_dependency "thor"
22
+ s.add_runtime_dependency "activemodel"
23
+
24
+ s.add_runtime_dependency "ambry", "~> 0.2.2"
25
+
26
+ end
@@ -0,0 +1,42 @@
1
+ if ENV["COVERAGE"]
2
+ require 'simplecov'
3
+ SimpleCov.start do
4
+ add_filter "/test/"
5
+ end
6
+ end
7
+
8
+ require "bundler/setup"
9
+ require "minitest/unit"
10
+ require 'minitest/autorun'
11
+ require "squirm"
12
+ require "ambry"
13
+ require "ambry/adapters/yaml"
14
+
15
+ $VERBOSE = true
16
+
17
+ require "squirm/model"
18
+
19
+
20
+ $squirm_test_connection ||= {dbname: "squirm_model_test"}
21
+ Squirm.connect $squirm_test_connection
22
+
23
+ class Module
24
+ def test(string, &block)
25
+ define_method "test_" + string.gsub(/[^a-z0-9,]/, "_"), block
26
+ end
27
+ end
28
+
29
+ def transaction
30
+ Squirm.transaction do
31
+ yield
32
+ Squirm.rollback
33
+ end
34
+ end
35
+
36
+ def with_api_for(model_class)
37
+ transaction do
38
+ Squirm.exec model_class.to_ddl
39
+ Squirm::Model.finalize
40
+ yield
41
+ end
42
+ end
@@ -0,0 +1,66 @@
1
+ require_relative "helper"
2
+
3
+ class ModelTest < MiniTest::Unit::TestCase
4
+
5
+ class Person
6
+ extend Squirm::Model
7
+ sample do |p|
8
+ p.id = 1.freeze
9
+ p.name = "John Doe"
10
+ end
11
+ end
12
+
13
+ test "should create" do
14
+ with_api_for(Person) do
15
+ person = Person.create(Person.sample.to_hash)
16
+ assert_instance_of Person, person
17
+ end
18
+ end
19
+
20
+ test "should get by id" do
21
+ with_api_for(Person) do
22
+ person = Person.create(Person.sample.to_hash)
23
+ assert_equal Person.sample.name, Person.get(1).name
24
+ end
25
+ end
26
+
27
+ test "should update" do
28
+ with_api_for(Person) do
29
+ person = Person.create(Person.sample.to_hash)
30
+ person.update name: "John Doe II"
31
+ assert_equal "John Doe II", Person.get(1).name
32
+ end
33
+ end
34
+
35
+ test "should delete" do
36
+ with_api_for(Person) do
37
+ begin
38
+ Person.create(Person.sample.to_hash)
39
+ assert Person.get(1)
40
+ Person.delete(1)
41
+ refute Person.get(1)
42
+ rescue Squirm::Model::NotFound
43
+ assert true
44
+ end
45
+ end
46
+ end
47
+
48
+ test "should save and create" do
49
+ with_api_for(Person) do
50
+ person = Person.sample(Person.new)
51
+ assert person.save
52
+ assert Person.get(person.id)
53
+ end
54
+ end
55
+
56
+ test "should save and update" do
57
+ with_api_for(Person) do
58
+ Person.create(Person.sample.to_hash)
59
+ assert person = Person.get(1)
60
+ person.name = "John Doe II"
61
+ assert person.save
62
+ assert_equal "John Doe II", Person.get(1).name
63
+ end
64
+ end
65
+
66
+ end
metadata ADDED
@@ -0,0 +1,134 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: squirm_model
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Norman Clarke
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-10-19 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: minitest
16
+ requirement: &70226482719700 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '2.6'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70226482719700
25
+ - !ruby/object:Gem::Dependency
26
+ name: squirm
27
+ requirement: &70226482718840 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>'
31
+ - !ruby/object:Gem::Version
32
+ version: 0.0.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70226482718840
36
+ - !ruby/object:Gem::Dependency
37
+ name: thor
38
+ requirement: &70226482718160 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70226482718160
47
+ - !ruby/object:Gem::Dependency
48
+ name: activemodel
49
+ requirement: &70226482717280 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *70226482717280
58
+ - !ruby/object:Gem::Dependency
59
+ name: ambry
60
+ requirement: &70226482716480 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ~>
64
+ - !ruby/object:Gem::Version
65
+ version: 0.2.2
66
+ type: :runtime
67
+ prerelease: false
68
+ version_requirements: *70226482716480
69
+ description: ! '"Squirm is an anti-ORM for database-loving programmers"'
70
+ email:
71
+ - norman@njclarke.com
72
+ executables:
73
+ - squirm
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - .gitignore
78
+ - .yardopts
79
+ - Gemfile
80
+ - Rakefile
81
+ - bin/squirm
82
+ - lib/squirm/migrator.rb
83
+ - lib/squirm/migrator/ambry.yml
84
+ - lib/squirm/migrator/column.rb
85
+ - lib/squirm/migrator/table.rb
86
+ - lib/squirm/migrator/template.rb
87
+ - lib/squirm/migrator/templates/sql/api/create.sql.erb
88
+ - lib/squirm/migrator/templates/sql/api/delete.sql.erb
89
+ - lib/squirm/migrator/templates/sql/api/get.sql.erb
90
+ - lib/squirm/migrator/templates/sql/api/update.sql.erb
91
+ - lib/squirm/migrator/templates/sql/columns/created.sql.erb
92
+ - lib/squirm/migrator/templates/sql/columns/foreign_key.sql.erb
93
+ - lib/squirm/migrator/templates/sql/columns/updated.sql.erb
94
+ - lib/squirm/migrator/templates/sql/index.sql.erb
95
+ - lib/squirm/migrator/templates/sql/layout.sql.erb
96
+ - lib/squirm/migrator/templates/sql/queries/procedure_info.sql
97
+ - lib/squirm/migrator/templates/sql/queries/table_info.sql
98
+ - lib/squirm/migrator/templates/sql/schema.sql.erb
99
+ - lib/squirm/migrator/templates/sql/table.sql.erb
100
+ - lib/squirm/model.rb
101
+ - lib/squirm/model/method_definer.rb
102
+ - lib/squirm/model/sample.rb
103
+ - lib/squirm/model/schema_functions.sql
104
+ - lib/squirm/model/version.rb
105
+ - squirm_model.gemspec
106
+ - test/helper.rb
107
+ - test/model_test.rb
108
+ homepage: http://github.com/bvision/squirm_model
109
+ licenses: []
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ! '>='
118
+ - !ruby/object:Gem::Version
119
+ version: '1.9'
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubyforge_project:
128
+ rubygems_version: 1.8.5
129
+ signing_key:
130
+ specification_version: 3
131
+ summary: ! '"An anti-ORM for database-loving programmers"'
132
+ test_files:
133
+ - test/helper.rb
134
+ - test/model_test.rb