squirm_model 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +19 -0
- data/.yardopts +4 -0
- data/Gemfile +8 -0
- data/Rakefile +18 -0
- data/bin/squirm +33 -0
- data/lib/squirm/migrator.rb +14 -0
- data/lib/squirm/migrator/ambry.yml +124 -0
- data/lib/squirm/migrator/column.rb +59 -0
- data/lib/squirm/migrator/table.rb +49 -0
- data/lib/squirm/migrator/template.rb +46 -0
- data/lib/squirm/migrator/templates/sql/api/create.sql.erb +20 -0
- data/lib/squirm/migrator/templates/sql/api/delete.sql.erb +10 -0
- data/lib/squirm/migrator/templates/sql/api/get.sql.erb +5 -0
- data/lib/squirm/migrator/templates/sql/api/update.sql.erb +14 -0
- data/lib/squirm/migrator/templates/sql/columns/created.sql.erb +12 -0
- data/lib/squirm/migrator/templates/sql/columns/foreign_key.sql.erb +7 -0
- data/lib/squirm/migrator/templates/sql/columns/updated.sql.erb +12 -0
- data/lib/squirm/migrator/templates/sql/index.sql.erb +2 -0
- data/lib/squirm/migrator/templates/sql/layout.sql.erb +5 -0
- data/lib/squirm/migrator/templates/sql/queries/procedure_info.sql +14 -0
- data/lib/squirm/migrator/templates/sql/queries/table_info.sql +23 -0
- data/lib/squirm/migrator/templates/sql/schema.sql.erb +3 -0
- data/lib/squirm/migrator/templates/sql/table.sql.erb +14 -0
- data/lib/squirm/model.rb +134 -0
- data/lib/squirm/model/method_definer.rb +54 -0
- data/lib/squirm/model/sample.rb +12 -0
- data/lib/squirm/model/schema_functions.sql +14 -0
- data/lib/squirm/model/version.rb +5 -0
- data/squirm_model.gemspec +26 -0
- data/test/helper.rb +42 -0
- data/test/model_test.rb +66 -0
- metadata +134 -0
data/.gitignore
ADDED
data/.yardopts
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -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
|
data/bin/squirm
ADDED
@@ -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,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,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,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 -%>
|
data/lib/squirm/model.rb
ADDED
@@ -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,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,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
|
data/test/helper.rb
ADDED
@@ -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
|
data/test/model_test.rb
ADDED
@@ -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
|