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