Skip to content

Commit 5dd90e5

Browse files
committed
wip #811
1 parent ca65acc commit 5dd90e5

5 files changed

Lines changed: 202 additions & 2 deletions

File tree

lapis/db/base_model.lua

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1116,7 +1116,21 @@ do
11161116
end
11171117
end
11181118
local res
1119-
if returning then
1119+
if opts and opts.on_conflict then
1120+
local insert_opts = {
1121+
on_conflict = opts.on_conflict
1122+
}
1123+
if return_all then
1124+
insert_opts.returning = "*"
1125+
elseif returning then
1126+
insert_opts.returning = returning
1127+
else
1128+
insert_opts.returning = {
1129+
self:primary_keys()
1130+
}
1131+
end
1132+
res = self.db.insert(self:table_name(), values, insert_opts)
1133+
elseif returning then
11201134
res = self.db.insert(self:table_name(), values, unpack(returning))
11211135
else
11221136
res = self.db.insert(self:table_name(), values, self:primary_keys())

lapis/db/base_model.moon

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,17 @@ class BaseModel
590590
returning or= {@primary_keys!}
591591
table.insert returning, k
592592

593-
res = if returning
593+
res = if opts and opts.on_conflict
594+
-- use options table API when on_conflict is specified
595+
insert_opts = { on_conflict: opts.on_conflict }
596+
if return_all
597+
insert_opts.returning = "*"
598+
elseif returning
599+
insert_opts.returning = returning
600+
else
601+
insert_opts.returning = { @primary_keys! }
602+
@db.insert @table_name!, values, insert_opts
603+
elseif returning
594604
@db.insert @table_name!, values, unpack returning
595605
else
596606
@db.insert @table_name!, values, @primary_keys!

spec/model_spec.moon

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,57 @@ describe "lapis.db.model", ->
424424
id: 101
425425
}, row1
426426

427+
it "should create model with on_conflict do_nothing", ->
428+
mock_query "INSERT", { { id: 101 } }
429+
430+
class Things extends Model
431+
432+
thing = Things\create { color: "blue" }, on_conflict: "do_nothing"
433+
434+
assert.same { id: 101, color: "blue" }, thing
435+
436+
assert_queries {
437+
[[INSERT INTO "things" ("color") VALUES ('blue') ON CONFLICT DO NOTHING RETURNING "id"]]
438+
}
439+
440+
it "should create model with on_conflict and returning", ->
441+
mock_query "INSERT", { { id: 101, height: 55 } }
442+
443+
class Things extends Model
444+
445+
thing = Things\create { color: "blue" }, on_conflict: "do_nothing", returning: { "height" }
446+
447+
assert.same { id: 101, color: "blue", height: 55 }, thing
448+
449+
assert_queries {
450+
[[INSERT INTO "things" ("color") VALUES ('blue') ON CONFLICT DO NOTHING RETURNING "id", "height"]]
451+
}
452+
453+
it "should create model with on_conflict and returning *", ->
454+
mock_query "INSERT", { { id: 101, color: "gotya", other: "field" } }
455+
456+
class Things extends Model
457+
458+
thing = Things\create { color: "blue" }, on_conflict: "do_nothing", returning: "*"
459+
460+
assert.same { id: 101, color: "gotya", other: "field" }, thing
461+
462+
assert_queries {
463+
[[INSERT INTO "things" ("color") VALUES ('blue') ON CONFLICT DO NOTHING RETURNING *]]
464+
}
465+
466+
it "should create timed model with on_conflict", ->
467+
mock_query "INSERT", { { id: 101 } }
468+
469+
class TimedThings extends Model
470+
@timestamp: true
471+
472+
thing = TimedThings\create { color: "blue" }, on_conflict: "do_nothing"
473+
474+
assert_queries {
475+
[[INSERT INTO "timed_things" ("color", "created_at", "updated_at") VALUES ('blue', '2013-08-13 06:56:40', '2013-08-13 06:56:40') ON CONFLICT DO NOTHING RETURNING "id"]]
476+
}
477+
427478
it "should refresh model", ->
428479
class Things extends Model
429480
mock_query "SELECT", { { id: 123 } }

spec/sqlite_spec.moon

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,58 @@ describe "lapis.db.sqlite", ->
654654
[[INSERT INTO "my_names" ("created_at", "name", "updated_at") VALUES ('2023-02-10 21:27:00', 'Nanette' || 2, '2023-02-10 21:27:00') RETURNING *]]
655655
}, query_log
656656

657+
it "Model:create with on_conflict do_nothing", ->
658+
-- First insert should succeed
659+
m1 = MyNames\create {
660+
name: "First"
661+
}
662+
663+
assert.same 1, m1.id
664+
665+
-- Create a unique index on name for conflict testing
666+
schema.create_index "my_names", "name", unique: true
667+
668+
-- Second insert with same name should be ignored
669+
m2 = MyNames\create {
670+
name: "First"
671+
}, on_conflict: "do_nothing"
672+
673+
-- Values come from input
674+
assert.same "First", m2.name
675+
676+
-- Only one row should exist
677+
assert.same 1, MyNames\count!
678+
679+
assert.same {
680+
[[INSERT INTO "my_names" ("created_at", "name", "updated_at") VALUES ('2023-02-10 21:27:00', 'First', '2023-02-10 21:27:00') RETURNING "id"]]
681+
[[CREATE UNIQUE INDEX "my_names_name_idx" ON "my_names" ("name")]]
682+
[[INSERT INTO "my_names" ("created_at", "name", "updated_at") VALUES ('2023-02-10 21:27:00', 'First', '2023-02-10 21:27:00') ON CONFLICT DO NOTHING RETURNING "id"]]
683+
[[SELECT COUNT(*) AS c FROM "my_names" ]]
684+
}, query_log
685+
686+
it "Model:create with on_conflict and returning *", ->
687+
-- First insert
688+
m1 = MyNames\create {
689+
name: "Test"
690+
}
691+
692+
schema.create_index "my_names", "name", unique: true
693+
694+
-- Conflicting insert with returning *
695+
m2 = MyNames\create {
696+
name: "Test"
697+
}, on_conflict: "do_nothing", returning: "*"
698+
699+
-- Only one row should exist
700+
assert.same 1, MyNames\count!
701+
702+
assert.same {
703+
[[INSERT INTO "my_names" ("created_at", "name", "updated_at") VALUES ('2023-02-10 21:27:00', 'Test', '2023-02-10 21:27:00') RETURNING "id"]]
704+
[[CREATE UNIQUE INDEX "my_names_name_idx" ON "my_names" ("name")]]
705+
[[INSERT INTO "my_names" ("created_at", "name", "updated_at") VALUES ('2023-02-10 21:27:00', 'Test', '2023-02-10 21:27:00') ON CONFLICT DO NOTHING RETURNING *]]
706+
[[SELECT COUNT(*) AS c FROM "my_names" ]]
707+
}, query_log
708+
657709
it "Model:update", ->
658710
-- handles when model no longer has record
659711
missing = MyNames\load { id: 99, name: "CowCat" }

spec_postgres/model_spec.moon

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,79 @@ describe "lapis.db.model", ->
277277
post\update user_id: db.raw "(case when false then 1234 else null end)"
278278
assert.same nil, post.user_id
279279

280+
describe "on_conflict", ->
281+
it "creates with on_conflict do_nothing", ->
282+
Likes\create_table!
283+
-- First insert should succeed
284+
like1 = Likes\create {
285+
user_id: 1
286+
post_id: 1
287+
count: 5
288+
}
289+
290+
assert.same 1, like1.user_id
291+
assert.same 1, like1.post_id
292+
assert.same 5, like1.count
293+
294+
-- Second insert with same primary key should be ignored
295+
like2 = Likes\create {
296+
user_id: 1
297+
post_id: 1
298+
count: 99
299+
}, on_conflict: "do_nothing"
300+
301+
-- When conflict occurs, the returned object has the input values
302+
-- but nothing was actually inserted
303+
assert.same 1, like2.user_id
304+
assert.same 1, like2.post_id
305+
assert.same 99, like2.count
306+
307+
-- Verify only one row exists with original count
308+
assert.same 1, Likes\count!
309+
original = Likes\find 1, 1
310+
assert.same 5, original.count
311+
312+
it "creates with on_conflict and returning *", ->
313+
Likes\create_table!
314+
-- First insert
315+
Likes\create {
316+
user_id: 1
317+
post_id: 1
318+
count: 5
319+
}
320+
321+
-- Conflicting insert with returning * - should return nothing when conflict
322+
like2 = Likes\create {
323+
user_id: 1
324+
post_id: 1
325+
count: 99
326+
}, on_conflict: "do_nothing", returning: "*"
327+
328+
-- Values come from input since RETURNING gives nothing on conflict
329+
assert.same 1, like2.user_id
330+
assert.same 1, like2.post_id
331+
332+
-- Verify only one row exists
333+
assert.same 1, Likes\count!
334+
335+
it "creates successfully with on_conflict when no conflict", ->
336+
Likes\create_table!
337+
-- Insert with on_conflict should work normally when no conflict
338+
like = Likes\create {
339+
user_id: 1
340+
post_id: 1
341+
count: 10
342+
}, on_conflict: "do_nothing"
343+
344+
assert.same 1, like.user_id
345+
assert.same 1, like.post_id
346+
assert.same 10, like.count
347+
348+
-- Verify row was inserted
349+
assert.same 1, Likes\count!
350+
found = Likes\find 1, 1
351+
assert.same 10, found.count
352+
280353
describe "delete", ->
281354
before_each ->
282355
Posts\create_table!

0 commit comments

Comments
 (0)