squirm_model 0.0.0

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.
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