duck_duck_duck 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/node/migrate_old ADDED
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env node
2
+
3
+ var _ = require('underscore')
4
+ ;
5
+
6
+ // Create databases: okdoki
7
+
8
+ var River = require('da_river').River
9
+ , Topogo = require('topogo').Topogo
10
+ , Customer = require('../Server/Customer/model').Customer
11
+ , Chat_Bot = require('../Server/Chat/Chat_Bot').Chat_Bot
12
+ , h = require('../test/helpers')
13
+ ;
14
+
15
+ var _ = require('underscore');
16
+
17
+ var cmd = (process.argv[2] || 'nothing')
18
+ , is_reset_user = cmd === 'reset_with_data'
19
+ , is_reset = cmd === 'reset' || is_reset_user
20
+ , is_up = is_reset || cmd === 'up'
21
+ , is_down = is_reset || cmd === 'down'
22
+ ;
23
+
24
+ if (!is_up && !is_down) {
25
+ console.log('Unknown cmd: ' + process.argv[2]);
26
+ process.exit(1);
27
+ }
28
+
29
+ var ok = {
30
+ list : [],
31
+ q: function (string) {
32
+ this.list.push(string
33
+ .replace(/\$id_size/g, Topogo.id_size)
34
+ .replace(/\$trashed_at/, " trashed_at bigint default null ")
35
+ );
36
+ return this;
37
+ }
38
+ };
39
+
40
+ function down(names, flow) {
41
+ var public = [];
42
+ var r = River.new(arguments);
43
+
44
+ if (!is_down)
45
+ return flow.finish(public);
46
+
47
+ _.each(names, function (n, i) {
48
+ if (n.indexOf('public.') === 0 ) {
49
+ public.push(n);
50
+ r.job('drop', n, function (j) {
51
+ Topogo.new(n).drop(j);
52
+ });
53
+ };
54
+ });
55
+
56
+ r.job('public tables', function (j, last) {
57
+ return j.finish(public);
58
+ });
59
+
60
+ r.run();
61
+ }
62
+
63
+ function up(flow) {
64
+ if (!is_up)
65
+ return flow.finish();
66
+
67
+ // ok.q("CREATE EXTENSION IF NOT EXISTS pgcrypto");
68
+
69
+ // ok.q(" \
70
+ // CREATE OR REPLACE FUNCTION public.json_get_varchar_array(j text, k text) \
71
+ // RETURNS varchar[] \
72
+ // AS $$ \
73
+ // import json; \
74
+ // d = json.loads(j or '{}'); \
75
+ // return d[k] if k in d else []; \
76
+ // $$ LANGUAGE plpython3u; \
77
+ // ");
78
+
79
+ // ok.q(" \
80
+ // CREATE OR REPLACE FUNCTION public.json_get_text_array(j text, k text) \
81
+ // RETURNS text[] \
82
+ // AS $$ \
83
+ // import json; \
84
+ // d = json.loads(j or '{}')[k]; \
85
+ // return d[k] if k in d else []; \
86
+ // $$ LANGUAGE plpython3u; \
87
+ // ");
88
+
89
+ // ok.q(" \
90
+ // CREATE OR REPLACE FUNCTION public.json_get(j text, k text) \
91
+ // RETURNS text \
92
+ // AS $$ \
93
+ // import json; \
94
+ // d = json.loads(j or '{}')[k]; \
95
+ // return d[k] if k in d else \"\"; \
96
+ // $$ LANGUAGE plpython3u; \
97
+ // ");
98
+
99
+ // ok.q(" \
100
+ // CREATE OR REPLACE FUNCTION public.json_merge(o text, n text) \
101
+ // RETURNS text \
102
+ // AS $$ \
103
+ // import json; \
104
+ // oj = json.loads(o or \"{}\"); \
105
+ // nj = json.loads(n or \"{}\"); \
106
+ // f = dict(list(oj.items()) + list(nj.items())); \
107
+ // return json.dumps(f); \
108
+ // $$ LANGUAGE plpython3u; \
109
+ // ");
110
+
111
+ // ok.q(" \
112
+ // CREATE OR REPLACE FUNCTION encode_pass_phrase(varchar) \
113
+ // RETURNS varchar \
114
+ // AS $$ \
115
+ // SELECT encode(digest($1, 'sha512'), 'hex') \
116
+ // $$ LANGUAGE SQL STRICT IMMUTABLE; \
117
+ // ");
118
+
119
+
120
+ } // end func up
121
+
122
+ function create(flow) {
123
+ console.log('Finished migrating the main db.');
124
+ if (!is_reset_user) {
125
+ return flow.finish();
126
+ }
127
+
128
+ var p = "pass phrase";
129
+ var report = function (j) {
130
+ console.log('Finished ' + j.group + ' ' + j.id);
131
+ };
132
+
133
+ var c_opts = {pass_phrase: p, confirm_pass_phrase: p, ip: '000.000.00'};
134
+
135
+ var r = River.new(arguments);
136
+ r.for_each_finish(report);
137
+ r.job('create:', 'go99', function (j) {
138
+ Customer.create(_.extend({screen_name: j.id}, c_opts), (j));
139
+ });
140
+
141
+ r.job('create:', 'dos', function (j) {
142
+ Customer.create(_.extend({screen_name: j.id}, c_opts), (j));
143
+ });
144
+
145
+ r.job('create bot:', '404', function (j) {
146
+ var c = j.river.reply_for('create:', 'go99');
147
+ Chat_Bot.create({owner_id: c.data.id, url: "https://okdoki-bot.herokuapp.com/test/404/404", name: j.id}, (j));
148
+ });
149
+
150
+ r.job('create bot:', 'ok', function (j) {
151
+ var c = j.river.reply_for('create:', 'go99');
152
+ Chat_Bot.create({owner_id: c.data.id, url: "https://okdoki-bot.herokuapp.com/test/ok", name: j.id}, (j));
153
+ });
154
+
155
+ r.job('create bot:', 'im', function (j) {
156
+ var c = j.river.reply_for('create:', 'dos');
157
+ Chat_Bot.create({owner_id: c.data.id, url: "https://okdoki-bot.herokuapp.com/test/im", name: j.id}, (j));
158
+ });
159
+
160
+ r.job('create bot:', 'not_json', function (j) {
161
+ var c = j.river.reply_for('create:', 'dos');
162
+ Chat_Bot.create({owner_id: c.data.id, url: "https://okdoki-bot.herokuapp.com/test/not_json", name: j.id}, (j));
163
+ });
164
+
165
+ r.run();
166
+
167
+ };
168
+
169
+ // // ==========================================================================================
170
+
171
+ // // ==========================================================================================
172
+
173
+
174
+ // ****************************************************************
175
+ // ****************** SQL Helpers *********************************
176
+ // ****************************************************************
177
+
178
+ var trashed_at = " trashed_at bigint default NULL ";
179
+
180
+ function varchar(o) { return " varchar( " + o + " ) "; }
181
+ var primary_key = " PRIMARY KEY ";
182
+ var serial = " serial ";
183
+ var not_null = " NOT NULL ";
184
+ var unique = " unique ";
185
+ var bigint = " bigint ";
186
+ var default_null= "default null";
187
+
188
+ function create_table(name, f) {
189
+ var vals = [];
190
+ var sql = function () {
191
+ vals.push(_.toArray(arguments));
192
+ };
193
+
194
+ f(sql);
195
+
196
+ return "CREATE TABLE IF NOT EXISTS " + name + " ( \n" + to_sql(vals) + " \n ); ";
197
+ }
198
+
199
+ function to_sql(vals) {
200
+ var lines = _.map(vals, function (row) { return row.join( ' ' ); });
201
+ return lines.join(", \n");
202
+ }
203
+
204
+
205
+
data/node/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "duck_duck_duck",
3
+ "version": "0.4.4",
4
+ "description": "My personal way to do multi-applet migrations.",
5
+ "main": "app.js",
6
+ "directories": {
7
+ "test": "test"
8
+ },
9
+ "scripts": {
10
+ "test": "bin/test"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git://github.com/da99/duck_duck_duck.git"
15
+ },
16
+ "dependencies": {
17
+ "underscore": "x.x.x",
18
+ "optimist": "x.x.x",
19
+ "da_river": "x.x.x",
20
+ "topogo": "x.x.x"
21
+ },
22
+ "keywords": [
23
+ "da99"
24
+ ],
25
+ "author": "da99",
26
+ "license": "MIT",
27
+ "readmeFilename": "README.md",
28
+ "gitHead": "849c5374d165e9fb10721becc31538c6f610ef59"
29
+ }
data/node/template.js ADDED
@@ -0,0 +1,22 @@
1
+
2
+ var Topogo = require("topogo").Topogo;
3
+ var River = require("da_river").River;
4
+
5
+ var table = "";
6
+ var m = module.exports = {};
7
+
8
+ m.migrate = function (dir, r) {
9
+
10
+ if (dir === 'down') {
11
+
12
+ Topogo.run('DROP TABLE IF EXISTS ' + table + ';', [], r);
13
+
14
+ } else {
15
+
16
+ var sql = 'CREATE TABLE IF NOT EXISTS ' + table + " ( \
17
+ \
18
+ );";
19
+ Topogo.run(sql, [], r);
20
+
21
+ }
22
+ };
data/node/test.sh ADDED
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env bash
2
+ # -*- bash -*-
3
+ #
4
+ set -u -e -o pipefail
5
+
6
+ export SCHEMA_TABLE="_test_schema"
7
+ export DUCK_TEMPLATE="../../template.js"
8
+
9
+ rm -f /tmp/duck_*
10
+ touch /tmp/duck_up
11
+ touch /tmp/duck_down
12
+ touch /tmp/duck_drop_it
13
+
14
+ function init {
15
+ rm -f "migrates/002-two.js"
16
+ rm -f "migrates/003-three.js"
17
+ rm -f "migrates/004-four.js"
18
+ rm -f "migrates/005-five.js"
19
+ rm -f "migrates/006-six.js"
20
+
21
+ cp "migrates/001-one.js" "migrates/002-two.js"
22
+ cp "migrates/001-one.js" "migrates/003-three.js"
23
+ }
24
+
25
+ function init_last_three {
26
+ cp "migrates/001-one.js" "migrates/004-four.js"
27
+ cp "migrates/001-one.js" "migrates/005-five.js"
28
+ cp "migrates/001-one.js" "migrates/006-six.js"
29
+ }
30
+
31
+ # ==== reset
32
+ node tests/helpers/drop.js
33
+
34
+ cd tests/user
35
+ ../../bin/duck_duck_duck up
36
+
37
+ cd ../raven_sword
38
+ ../../bin/duck_duck_duck up
39
+
40
+ cd ../praying_mantis
41
+ rm -f "migrates/008-eight.js"
42
+ rm -f "migrates/010-ten.js"
43
+ cp "migrates/002-two.js" "migrates/004-four.js"
44
+ cp "migrates/002-two.js" "migrates/006-six.js"
45
+ ../../bin/duck_duck_duck up
46
+ cp "migrates/002-two.js" "migrates/008-eight.js"
47
+ cp "migrates/002-two.js" "migrates/010-ten.js"
48
+ ../../bin/duck_duck_duck down
49
+
50
+ cd ../lone_wolf
51
+ init
52
+ ../../bin/duck_duck_duck up
53
+ init_last_three
54
+ ../../bin/duck_duck_duck up
55
+
56
+ cd ../laughing_octopus
57
+ rm -rf migrates
58
+ ../../bin/duck_duck_duck create one
59
+ ../../bin/duck_duck_duck create two
60
+ ../../bin/duck_duck_duck create three
61
+
62
+ # test .drop/.create
63
+ cd ../screaming_mantis
64
+ ../../bin/duck_duck_duck up
65
+ ../../bin/duck_duck_duck down
66
+
67
+ cd ../liquid
68
+ ../../bin/duck_duck_duck up
69
+ ../../bin/duck_duck_duck drop_it
70
+
71
+ cd ../..
72
+ bin/duck_duck_duck list > /tmp/duck_list
73
+ mocha tests/duck_duck_duck.js
74
+
75
+
76
+
data/node/tests.js ADDED
@@ -0,0 +1,161 @@
1
+
2
+ var _ = require('underscore')
3
+ , Topogo = require('topogo').Topogo
4
+ , River = require('da_river').River
5
+ , assert = require('assert')
6
+ , fs = require('fs')
7
+ ;
8
+
9
+ var does = function (name, func) {
10
+ if (func.length !== 1)
11
+ throw new Error('Test func requires done: ' + name);
12
+ return it(name, func);
13
+ };
14
+
15
+ var applets_list = {};
16
+ function applets(func) {
17
+ if (applets_list.length)
18
+ return func(applets_list);
19
+
20
+ River.new()
21
+ .job(function (j) {
22
+ Topogo.run('SELECT * FROM _test_schema', [], j);
23
+ })
24
+ .run(function (r, recs) {
25
+ _.each(recs, function (r, i) {
26
+ applets_list[r.name] = r.version;
27
+ });
28
+ func(applets_list);
29
+ });
30
+ }
31
+
32
+ describe( 'Before first migrate:', function () {
33
+
34
+ it( 'creates schema table', function (done) {
35
+ River.new(null)
36
+ .job(function (j) {
37
+ Topogo.run('SELECT * FROM _test_schema', [], j);
38
+ })
39
+ .job(function (j, last) {
40
+ assert.equal(last.length > 0, true);
41
+ done();
42
+ })
43
+ .run();
44
+ });
45
+
46
+ it( 'creates rows with: name, version', function (done) {
47
+ River.new(null)
48
+ .job(function (j) {
49
+ Topogo.run('SELECT * FROM _test_schema', [], j);
50
+ })
51
+ .job(function (j, last) {
52
+ assert.deepEqual(_.keys(last[0]), ['name', 'version']);
53
+ done();
54
+ })
55
+ .run();
56
+ });
57
+
58
+ }); // === end desc
59
+
60
+ describe( 'Migrate up:', function () {
61
+
62
+ does( 'updates version to latest migrate', function (done) {
63
+ River.new()
64
+
65
+ .job(function (j) {
66
+ Topogo.run('SELECT * FROM _test_schema', [], j);
67
+ })
68
+
69
+ .job(function (j, last) {
70
+ assert(last[0].version, 3);
71
+ done();
72
+ })
73
+
74
+ .run();
75
+ });
76
+
77
+ it( 'migrates files higher, but not equal, of current version', function () {
78
+ var contents = fs.readFileSync('/tmp/duck_up').toString().trim();
79
+ assert.equal(contents, "+1+2+3+4+5+6");
80
+ });
81
+
82
+ }); // === end desc
83
+
84
+ describe( 'Migrate down:', function () {
85
+
86
+ var contents = null;
87
+
88
+ before(function () {
89
+ contents = fs.readFileSync('/tmp/duck_down').toString().trim();
90
+ });
91
+
92
+ it( 'runs migrates in reverse order', function () {
93
+ assert.equal(contents, "+2+4+6-6-4-2");
94
+ });
95
+
96
+ it( 'does not run down migrates from later versions', function () {
97
+ // This tests is the same as "runs migrates in reverse order"
98
+ assert.equal(contents, "+2+4+6-6-4-2");
99
+ });
100
+
101
+ does( 'update version to one less than earlier version', function (done) {
102
+ River.new(null)
103
+ .job(function (j) {
104
+ Topogo.run('SELECT * FROM _test_schema', [], j);
105
+ })
106
+ .job(function (j, last) {
107
+ var pm = _.find(last, function (rec) {
108
+ return rec.name === 'praying_mantis';
109
+ });
110
+ assert.equal(pm.version, 0);
111
+ done();
112
+ })
113
+ .run();
114
+ });
115
+
116
+ }); // === end desc
117
+
118
+
119
+ describe( 'create ...', function () {
120
+
121
+ it( 'create a file', function () {
122
+ var contents = fs.readFileSync("tests/laughing_octopus/migrates/001-one.js").toString();
123
+ assert.equal(contents.indexOf('var ') > -1, true);
124
+ });
125
+
126
+ it( 'creates file in successive order', function () {
127
+ var contents = fs.readFileSync("tests/laughing_octopus/migrates/002-two.js").toString();
128
+ assert.equal(contents.indexOf('var ') > -1, true);
129
+ var contents = fs.readFileSync("tests/laughing_octopus/migrates/003-three.js").toString();
130
+ assert.equal(contents.indexOf('var ') > -1, true);
131
+ });
132
+
133
+ }); // === end desc
134
+
135
+
136
+
137
+ describe( 'drop_it', function () {
138
+
139
+ it( 'migrates down', function () {
140
+ var contents = fs.readFileSync("/tmp/duck_drop_it").toString();
141
+ assert.deepEqual(contents, "drop_it");
142
+ });
143
+
144
+ does( 'removes entry from schema', function (done) {
145
+ applets(function (list) {
146
+ assert.deepEqual(list.liquid, undefined);
147
+ done();
148
+ });
149
+ });
150
+
151
+ }); // === end desc
152
+
153
+
154
+ describe( 'list', function () {
155
+
156
+ it( 'outputs schema table on each line: VER_NUM NAME', function () {
157
+ var contents = fs.readFileSync("/tmp/duck_list").toString().split("\n");
158
+ assert.deepEqual(!!contents[0].match(/\d user/), true);
159
+ assert.deepEqual(!!contents[1].match(/\d raven_sword/), true);
160
+ });
161
+ }); // === end desc
@@ -0,0 +1,150 @@
1
+
2
+ require 'sequel'
3
+ require 'Exit_0'
4
+
5
+ schema = ENV['SCHEMA_TABLE'] = '_test_schema'
6
+ DB = Sequel.connect ENV['DATABASE_URL']
7
+ MODELS = Dir.glob('*/migrates').map { |dir| File.basename File.dirname(dir) }
8
+
9
+ # === Reset tables ===========================================================
10
+ def reset
11
+ tables = MODELS + [ENV['SCHEMA_TABLE']]
12
+ tables.each { |t|
13
+ DB << "DROP TABLE IF EXISTS #{t.inspect};"
14
+ }
15
+ end
16
+
17
+ reset
18
+
19
+ # === Helpers ================================================================
20
+ def get *args
21
+ field = args.last.is_a?(Symbol) ? args.pop : nil
22
+ rows = DB[*args].all
23
+ if field
24
+ rows.map { |row| row[field] }
25
+ else
26
+ rows
27
+ end
28
+ end
29
+
30
+ def versions mod
31
+ Dir.glob("#{mod}/migrates/*").map { |file|
32
+ file[/\/(\d{4})[^\/]+\.sql$/] && $1.to_i
33
+ }.sort
34
+ end
35
+
36
+ # === Specs ==================================================================
37
+
38
+ describe "create" do
39
+
40
+ before {
41
+ tmp_dir = '/tmp/ddd_ver'
42
+ Exit_0("rm -fr #{tmp_dir}")
43
+ Exit_0("mkdir -p #{tmp_dir}")
44
+ @dir = tmp_dir
45
+ }
46
+
47
+ it "names the file in successive file versions: 0000-....sql" do
48
+ Dir.chdir(@dir) {
49
+ Exit_0("duck_duck_duck create MOD table_1")
50
+ Exit_0("duck_duck_duck create MOD table_2")
51
+
52
+ Exit_0("touch MOD/migrates/0022-skip_zero.sql")
53
+ Exit_0("duck_duck_duck create MOD table_3")
54
+
55
+ Exit_0("touch MOD/migrates/0091-skip_zero.sql")
56
+ Exit_0("duck_duck_duck create MOD table_100")
57
+
58
+ File.should.exists('MOD/migrates/0010-table_1.sql')
59
+ File.should.exists('MOD/migrates/0020-table_2.sql')
60
+ File.should.exists('MOD/migrates/0030-table_3.sql')
61
+ File.should.exists('MOD/migrates/0100-table_100.sql')
62
+ }
63
+ end
64
+
65
+ end # === describe create
66
+
67
+ describe 'up model' do
68
+
69
+ before { reset }
70
+
71
+ it( 'updates version to latest migration' ) do
72
+ Exit_0("duck_duck_duck up 0010_model")
73
+ get('SELECT * FROM _test_schema').
74
+ first[:version].should == versions('0010_model').last
75
+ end
76
+
77
+ it 'does not run migrations from previous versions' do
78
+ Exit_0("duck_duck_duck migrate_schema")
79
+ DB << File.read("0010_model/migrates/0010-table.sql").split('-- DOWN').first
80
+ DB << "INSERT INTO #{schema.inspect} VALUES ('0010_model', '20');"
81
+ Exit_0("duck_duck_duck up 0010_model")
82
+ get('SELECT * FROM "0010_model"', :title).
83
+ should == ['record 30', 'record 40', 'record 50']
84
+ end
85
+
86
+ end # === describe up model
87
+
88
+ describe 'down model' do
89
+
90
+ before { reset }
91
+
92
+ it 'leaves version to 0' do
93
+ Exit_0("duck_duck_duck up 0010_model")
94
+ Exit_0("duck_duck_duck down 0010_model")
95
+ get(%^SELECT * FROM #{schema.inspect} WHERE name = '0010_model'^, :version).last.
96
+ should == 0
97
+ end
98
+
99
+ it 'runs migrates in reverse order' do
100
+ Exit_0("duck_duck_duck up 0020_model")
101
+ Exit_0("duck_duck_duck down 0020_model")
102
+ get('SELECT * FROM "0020_model"', :title).
103
+ should == ['record 20', 'record 30', 'DROP record 30', 'DROP record 20', 'DROP 0020_model']
104
+ end
105
+
106
+ it 'does not run down migrates from later versions' do
107
+ Exit_0("duck_duck_duck migrate_schema")
108
+ DB << File.read("0020_model/migrates/0010-table.sql").split('-- DOWN').first
109
+ DB << "INSERT INTO #{schema.inspect} VALUES ('0020_model', '20');"
110
+ DB << "UPDATE #{schema} SET version = '20' WHERE name = '0020_model';"
111
+ Exit_0("duck_duck_duck down 0020_model")
112
+ get('SELECT * FROM "0020_model"', :title).
113
+ should == ['DROP record 20', 'DROP 0020_model']
114
+ end
115
+
116
+ end # === describe down model
117
+
118
+ describe "up" do
119
+
120
+ before { reset }
121
+
122
+ it "migrates all models" do
123
+ Exit_0("duck_duck_duck up")
124
+ get("SELECT * FROM #{schema.inspect} ORDER BY name").
125
+ should == [
126
+ {:name=>'0010_model',:version=>50},
127
+ {:name=>'0020_model',:version=>30},
128
+ {:name=>'0030_model',:version=>30}
129
+ ]
130
+ end
131
+
132
+ end # describe up
133
+
134
+ describe 'down' do
135
+
136
+ before { reset }
137
+
138
+ it "migrates down all models" do
139
+ Exit_0("duck_duck_duck up")
140
+ Exit_0("duck_duck_duck down")
141
+ get("SELECT * FROM #{schema.inspect} ORDER BY name").
142
+ should == [
143
+ {:name=>'0010_model',:version=>0},
144
+ {:name=>'0020_model',:version=>0},
145
+ {:name=>'0030_model',:version=>0}
146
+ ]
147
+ end
148
+
149
+ end # === describe down
150
+
@@ -0,0 +1,4 @@
1
+
2
+ require 'Bacon_Colored'
3
+ require 'duck_duck_duck'
4
+ require 'pry'
@@ -0,0 +1,13 @@
1
+
2
+ CREATE TABLE "0010_model" (
3
+ id serial NOT NULL PRIMARY KEY,
4
+ title varchar(100)
5
+ );
6
+
7
+
8
+ -- DOWN
9
+
10
+ INSERT INTO "0010_model" (title)
11
+ VALUES ('DROP 0010_model');
12
+
13
+
@@ -0,0 +1,11 @@
1
+
2
+ INSERT INTO "0010_model" (title)
3
+ VALUES ('record 20');
4
+
5
+
6
+ -- DOWN
7
+
8
+ INSERT INTO "0010_model" (title)
9
+ VALUES ('DROP record 20: 0010_model');
10
+
11
+
@@ -0,0 +1,11 @@
1
+
2
+ INSERT INTO "0010_model" (title)
3
+ VALUES ('record 30');
4
+
5
+
6
+ -- DOWN
7
+
8
+ INSERT INTO "0010_model" (title)
9
+ VALUES ('DROP record 30');
10
+
11
+
@@ -0,0 +1,11 @@
1
+
2
+ INSERT INTO "0010_model" (title)
3
+ VALUES ('record 40');
4
+
5
+
6
+ -- DOWN
7
+
8
+ INSERT INTO "0010_model" (title)
9
+ VALUES ('DROP record 40');
10
+
11
+