Added plan of tests

This commit is contained in:
Alexander Pivkin 2025-12-20 10:21:45 +03:00
parent a889e1c7dd
commit 640a6d2a46
171 changed files with 57454 additions and 0 deletions

View File

@ -0,0 +1,36 @@
# Code of Conduct
This code of conduct outlines our expectations for all those who participate in our open source projects and communities (community programs), as well as the consequences for unacceptable behaviour. We invite all those who participate to help us create safe and positive experiences for everyone. Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society.
## How to behave
The following behaviours are expected and requested of all community members:
- Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community.
- Exercise consideration, respect and empathy in your speech and actions. Remember, we have all been through different stages of learning when adopting technologies.
- Refrain from demeaning, discriminatory, or harassing behaviour and speech.
- Disagreements on things are fine, argumentative behaviour or trolling are not.
## How not to behave
- Do not perform threats of violence or use violent language directed against another person.
- Do not make jokes of sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory nature, or use language of this nature.
- Do not post or display sexually explicit or violent material.
- Do not post or threaten to post other people's personally identifying information ("doxing").
- Do not make personal insults, particularly those related to gender, sexual orientation, race, religion, or disability.
- Do not engage in sexual attention. This includes, sexualised comments or jokes and sexual advances.
- Do not advocate for, or encourage, any of the above behaviour.
Please take into account that online communities bring together people from many different cultures and backgrounds. It's important to understand that sometimes the combination of cultural differences and online interaction can lead to misunderstandings. That is why having empathy is very important.
## How to report issues
If someone is acting inappropriately or violating this Code of Conduct in any shape or form, and they are not receptive to your feedback or you prefer not to confront them, please reach out to JetBrains via <codeofconduct@jetbrains.com>
## Consequences of Unacceptable Behaviour
Unacceptable behaviour from any community member will not be tolerated. Anyone asked to stop unacceptable behaviour is expected to comply immediately. If a community member engages in unacceptable behaviour, JetBrains and/or community organisers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning.
## License and attribution
The license is based off of The Citizen Code of Conduct is distributed by Stumptown Syndicate under a Creative Commons Attribution-ShareAlike license.

View File

@ -0,0 +1,6 @@
# Blog with Go Templates
This is a blog with templates. It uses the `html/template` package to generate web pages.
It uses the Chi router to set up the routes and an SQLite database to store the articles.
Find the tutorial [here](https://blog.jetbrains.com/go/2022/11/08/build-a-blog-with-go-templates/).

Binary file not shown.

View File

@ -0,0 +1,117 @@
package main
import "database/sql"
func connect() (*sql.DB, error) {
var err error
db, err = sql.Open("sqlite3", "./data.sqlite")
if err != nil {
return nil, err
}
sqlStmt := `
create table if not exists articles (id integer not null primary key autoincrement, title text, content text);
`
_, err = db.Exec(sqlStmt)
if err != nil {
return nil, err
}
return db, nil
}
func dbCreateArticle(article *Article) error {
query, err := db.Prepare("insert into articles(title,content) values (?,?)")
defer query.Close()
if err != nil {
return err
}
_, err = query.Exec(article.Title, article.Content)
if err != nil {
return err
}
return nil
}
func dbGetAllArticles() ([]*Article, error) {
query, err := db.Prepare("select id, title, content from articles")
defer query.Close()
if err != nil {
return nil, err
}
result, err := query.Query()
if err != nil {
return nil, err
}
articles := make([]*Article, 0)
for result.Next() {
data := new(Article)
err := result.Scan(
&data.ID,
&data.Title,
&data.Content,
)
if err != nil {
return nil, err
}
articles = append(articles, data)
}
return articles, nil
}
func dbGetArticle(articleID string) (*Article, error) {
query, err := db.Prepare("select id, title, content from articles where id = ?")
defer query.Close()
if err != nil {
return nil, err
}
result := query.QueryRow(articleID)
data := new(Article)
err = result.Scan(&data.ID, &data.Title, &data.Content)
if err != nil {
return nil, err
}
return data, nil
}
func dbUpdateArticle(id string, article *Article) error {
query, err := db.Prepare("update articles set (title, content) = (?,?) where id=?")
defer query.Close()
if err != nil {
return err
}
_, err = query.Exec(article.Title, article.Content, id)
if err != nil {
return err
}
return nil
}
func dbDeleteArticle(id string) error {
query, err := db.Prepare("delete from articles where id=?")
defer query.Close()
if err != nil {
return err
}
_, err = query.Exec(id)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,8 @@
module GoBlog
go 1.19
require (
github.com/go-chi/chi/v5 v5.0.7
github.com/mattn/go-sqlite3 v1.14.15
)

View File

@ -0,0 +1,4 @@
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -0,0 +1,221 @@
package main
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
_ "github.com/mattn/go-sqlite3"
"html/template"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"time"
)
var router *chi.Mux
var db *sql.DB
type Article struct {
ID int `json:"id"`
Title string `json:"title"`
Content template.HTML `json:"content"`
}
func catch(err error) {
if err != nil {
fmt.Println(err)
panic(err)
}
}
func init() {
router = chi.NewRouter()
router.Use(middleware.Recoverer)
var err error
db, err = connect()
catch(err)
}
func main() {
router = chi.NewRouter()
router.Use(middleware.Recoverer)
var err error
db, err = connect()
catch(err)
router.Use(ChangeMethod)
router.Get("/", GetAllArticles)
router.Post("/upload", UploadHandler) // Add this
router.Get("/images/*", ServeImages) // Add this
router.Route("/articles", func(r chi.Router) {
r.Get("/", NewArticle)
r.Post("/", CreateArticle)
r.Route("/{articleID}", func(r chi.Router) {
r.Use(ArticleCtx)
r.Get("/", GetArticle) // GET /articles/1234
r.Put("/", UpdateArticle) // PUT /articles/1234
r.Delete("/", DeleteArticle) // DELETE /articles/1234
r.Get("/edit", EditArticle) // GET /articles/1234/edit
})
})
err = http.ListenAndServe(":8005", router)
catch(err)
}
func UploadHandler(w http.ResponseWriter, r *http.Request) {
const MAX_UPLOAD_SIZE = 10 << 20
r.Body = http.MaxBytesReader(w, r.Body, MAX_UPLOAD_SIZE)
if err := r.ParseMultipartForm(MAX_UPLOAD_SIZE); err != nil {
http.Error(w, "The uploaded file is too big. Please choose an file that's less than 10MB in size", http.StatusBadRequest)
return
}
file, fileHeader, err := r.FormFile("file")
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer file.Close()
// Create the uploads folder if it doesn't already exist
err = os.MkdirAll("./images", os.ModePerm)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Create a new file in the uploads directory
filename := fmt.Sprintf("/images/%d%s", time.Now().UnixNano(), filepath.Ext(fileHeader.Filename))
dst, err := os.Create("." + filename)
if err != nil {
fmt.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer dst.Close()
// Copy the uploaded file to the specified destination
_, err = io.Copy(dst, file)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Println(filename)
response, _ := json.Marshal(map[string]string{"location": filename})
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
w.Write(response)
}
func ServeImages(w http.ResponseWriter, r *http.Request) {
fmt.Println(r.URL)
fs := http.StripPrefix("/images/", http.FileServer(http.Dir("./images")))
fs.ServeHTTP(w, r)
}
func ChangeMethod(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
switch method := r.PostFormValue("_method"); method {
case http.MethodPut:
fallthrough
case http.MethodPatch:
fallthrough
case http.MethodDelete:
r.Method = method
default:
}
}
next.ServeHTTP(w, r)
})
}
func ArticleCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
articleID := chi.URLParam(r, "articleID")
article, err := dbGetArticle(articleID)
if err != nil {
fmt.Println(err)
http.Error(w, http.StatusText(404), 404)
return
}
ctx := context.WithValue(r.Context(), "article", article)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func GetAllArticles(w http.ResponseWriter, r *http.Request) {
articles, err := dbGetAllArticles()
catch(err)
t, _ := template.ParseFiles("templates/base.html", "templates/index.html")
err = t.Execute(w, articles)
catch(err)
}
func NewArticle(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("templates/base.html", "templates/new.html")
err := t.Execute(w, nil)
catch(err)
}
func CreateArticle(w http.ResponseWriter, r *http.Request) {
title := r.FormValue("title")
content := r.FormValue("content")
article := &Article{
Title: title,
Content: template.HTML(content),
}
err := dbCreateArticle(article)
catch(err)
http.Redirect(w, r, "/", http.StatusFound)
}
func GetArticle(w http.ResponseWriter, r *http.Request) {
article := r.Context().Value("article").(*Article)
t, _ := template.ParseFiles("templates/base.html", "templates/article.html")
err := t.Execute(w, article)
catch(err)
}
func EditArticle(w http.ResponseWriter, r *http.Request) {
article := r.Context().Value("article").(*Article)
t, _ := template.ParseFiles("templates/base.html", "templates/edit.html")
err := t.Execute(w, article)
catch(err)
}
func UpdateArticle(w http.ResponseWriter, r *http.Request) {
article := r.Context().Value("article").(*Article)
title := r.FormValue("title")
content := r.FormValue("content")
newArticle := &Article{
Title: title,
Content: template.HTML(content),
}
fmt.Println(newArticle.Content)
err := dbUpdateArticle(strconv.Itoa(article.ID), newArticle)
catch(err)
http.Redirect(w, r, fmt.Sprintf("/articles/%d", article.ID), http.StatusFound)
}
func DeleteArticle(w http.ResponseWriter, r *http.Request) {
article := r.Context().Value("article").(*Article)
err := dbDeleteArticle(strconv.Itoa(article.ID))
catch(err)
http.Redirect(w, r, "/", http.StatusFound)
}

View File

@ -0,0 +1,15 @@
{{define "title"}}{{.Title}}{{end}}
{{define "scripts"}}{{end}}
{{define "body"}}
<h1>{{.Title}} </h1>
<div>
{{.Content}}
</div>
<div>
<a href="/articles/{{.ID}}/edit">Edit</a>
<form action="/articles/{{.ID}}" method="post">
<input type="hidden" name="_method" value="DELETE">
<button type="submit">Delete</button>
</form>
</div>
{{end}}

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ template "title" . }}</title>
{{ template "scripts" }}
</head>
<body>
{{ template "body" . }}
</body>
</html>

View File

@ -0,0 +1,26 @@
{{define "title"}}Create new article{{end}}
{{define "scripts"}}
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
<script>
tinymce.init({
selector: '#mytextarea',
plugins: 'image',
toolbar: 'undo redo | blocks | image | ' +
'bold italic backcolor | alignleft aligncenter ' +
'alignright alignjustify | bullist numlist outdent indent | ' +
'removeformat | help',
images_upload_url: "/upload",
relative_urls : false,
remove_script_host : false,
convert_urls : true,
});
</script>
{{end}}
{{define "body"}}
<form method="post" action="/articles/{{.ID}}">
<input type="text" name="title" value="{{.Title}}">
<textarea id="mytextarea" name="content">{{.Content}}</textarea>
<input type="hidden" name="_method" value="PUT">
<button id="submit" type="submit" onclick="submitForm()">Edit</button>
</form>
{{end}}

View File

@ -0,0 +1,16 @@
{{define "title"}}All articles{{end}}
{{define "scripts"}}{{end}}
{{define "body"}}
{{if eq (len .) 0}}
Nothing to see here
{{end}}
{{range .}}
<div>
<a href="/articles/{{.ID}}">{{.Title}}</a>
</div>
{{end}}
<p>
<a href="/articles">Create new article</a>
</p>
{{end}}

View File

@ -0,0 +1,25 @@
{{define "title"}}Create new article{{end}}
{{define "scripts"}}
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
<script>
tinymce.init({
selector: '#mytextarea',
plugins: 'image',
toolbar: 'undo redo | blocks | image | ' +
'bold italic backcolor | alignleft aligncenter ' +
'alignright alignjustify | bullist numlist outdent indent | ' +
'removeformat | help',
images_upload_url: "/upload",
relative_urls : false,
remove_script_host : false,
convert_urls : true,
});
</script>
{{end}}
{{define "body"}}
<form method="post" action="/articles">
<input type="text" name="title" placeholder="Enter the title">
<textarea id="mytextarea" name="content"></textarea>
<button id="submit" type="submit">Create</button>
</form>
{{end}}

201
go-code-samples/LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,5 @@
[![JetBrains team project](https://jb.gg/badges/team.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub)
# go-code-samples
This repository contains code samples for tutorials published on [the GoLand blog](https://blog.jetbrains.com/go/category/tutorials/). Each folder has a separate README with a link to the corresponding article.

View File

@ -0,0 +1,3 @@
module error-handling
go 1.20

View File

@ -0,0 +1,91 @@
package main
import (
"errors"
"io/fs"
"log"
)
func main() {
log.SetFlags(0) // no time stamp
// Read a single file
log.SetPrefix("Reading a single file: ")
_, err := ReadFile("no/file")
if err != nil {
log.Println("err = ", err)
}
// Unwrap the error returned by os.Open()
log.Println("errors.Unwrap(err) = ", errors.Unwrap(err))
// Confirm that the error is, or wraps, an fs.ErrNotExist error
log.Println("err is fs.ErrNotExist:", errors.Is(err, fs.ErrNotExist))
// Confirm that the error is, or wraps, an fs.PathError.
// errors.As() assigns the unwrapped PathError to target.
// This allows reading PathError's Path field.
target := &fs.PathError{}
if errors.As(err, &target) {
log.Printf("err as PathError: path is '%s'\n", target.Path)
log.Printf("err as PathError: op is '%s'\n", target.Op)
}
// Read files concurrently - handling context errors
log.SetPrefix("Reading files concurrently: ")
_, err = ReadFilesConcurrently([]string{"no/file/a", "no/file/b", "no/file/c"})
log.Println("err = ", err)
// Read multiple files
log.SetPrefix("Reading multiple files: ")
// Passing an empty slice triggers a plain error
_, err = ReadFiles([]string{})
log.Println("err = ", err)
// Passing multiple paths of non-existing files triggers a joined error
_, err = ReadFiles([]string{"no/file/a", "no/file/b", "no/file/c"})
log.Println("joined errors = ", err)
// Unwrap the errors inside the joined error
// A joined error does not have the method "func Unwrap() error"
// because it does not wrap a single error but rather a slice of errors.
// Therefore, errors.Unwrap() cannot unwrap the errors and returns nil.
log.Println("errors.Unwrap(err) = ", errors.Unwrap(err))
// To unwrap a joined error, you can type-assert that err has
// an Unwrap() method that returns a slice of errors.
e, ok := err.(interface{ Unwrap() []error })
if ok {
log.Println("e.Unwrap() = ", e.Unwrap())
}
// Network errors
log.SetPrefix("Network errors: ")
err = connectToTCPServer()
log.Println("err = ", err)
// Recover from a panic
log.SetPrefix("Recovering from a panic: ")
// This example is at the end of main, because the panic
// causes main to exit. Only the deferred function is
// called before exiting.
defer func() {
// Is this func invoked from a panic?
if r := recover(); r != nil {
// Yes: recover from the panic
log.Printf("isValidPath panicked: error is '%v'\n", r)
// ...
}
}()
// isValidPath panics because of an invalid regexp.
if isValidPath("/path/to/file") {
_, _ = ReadFile("/path/to/file")
}
}

View File

@ -0,0 +1,36 @@
package main
import (
"errors"
"fmt"
"log"
"net"
)
// Connect to a TCP server and check the error. use errors.As() to unwrap the net.OpError, and test if the error is transient.
func connectToTCPServer() error {
var err error
var conn net.Conn
for retry := 3; retry > 0; retry-- {
conn, err = net.Dial("tcp", "127.0.0.1:12345")
if err != nil {
// Check if err is a net.OpError
opErr := &net.OpError{}
if errors.As(err, &opErr) {
log.Println("err is net.OpError:", opErr.Error())
// test if the error is temporary
if opErr.Temporary() {
log.Printf("Retrying...\n")
continue
}
retry = 0
}
}
}
if err != nil {
return fmt.Errorf("connect failed: %w", err)
}
defer conn.Close()
// send or receive data
return nil
}

View File

@ -0,0 +1,30 @@
package main
import (
"errors"
"fmt"
"io"
"os"
)
func ReadFile(path string) ([]byte, error) {
if path == "" {
// Create an error with errors.New()
return nil, errors.New("path is empty")
}
f, err := os.Open(path)
if err != nil {
// Wrap the error.
// If the format string uses %w to format the error,
// fmt.Errorf() returns an error that has the
// method "func Unwrap() error" implemented.
return nil, fmt.Errorf("open failed: %w", err)
}
defer f.Close()
buf, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("read failed: %w", err)
}
return buf, nil
}

View File

@ -0,0 +1,33 @@
package main
import (
"errors"
"fmt"
)
func ReadFiles(paths []string) ([][]byte, error) {
var errs error
var contents [][]byte
if len(paths) == 0 {
// Create a new error with fmt.Errorf() (but without using %w):
return nil, fmt.Errorf("no paths provided: paths slice is %v", paths)
}
for _, path := range paths {
content, err := ReadFile(path)
if err != nil {
// Join all errors that occur into errs.
// The returned error type implements method "func Unwrap() []error".
// (Note that the return type is a slice.)
errs = errors.Join(errs, fmt.Errorf("reading %s failed: %w", path, err))
continue
}
contents = append(contents, content)
}
// Some files may have been read, some may have failed to be read.
// Therefore, ReadFiles returns both return values, regardless
// of whether there have been errors.
return contents, errs
}

View File

@ -0,0 +1,62 @@
package main
import (
"context"
"fmt"
"log"
"math/rand"
"sync"
"time"
)
// Goal: Read multiple files concurrently. All files must be read successfully.
// If one goroutine fails, cancel all the other goroutines.
//
// The cancelled goroutines can inspect the error from canceling the context with ctx.Err()..
func ReadFilesConcurrently(paths []string) ([][]byte, error) {
var contents [][]byte
ctx, cancel := context.WithCancelCause(context.Background())
defer cancel(nil)
resCh := make(chan []byte)
wg := sync.WaitGroup{}
for _, path := range paths {
wg.Add(1)
go func(ctx context.Context, cancel context.CancelCauseFunc, p string, resCh chan<- []byte, wg *sync.WaitGroup) {
time.Sleep(time.Duration(rand.Intn(10)) * time.Microsecond) // simulate workload
select {
case <-ctx.Done():
log.Printf("ReadFilesConcurrently (goroutine): Context canceled for path %s: %v", p, ctx.Err())
wg.Done()
return
default:
// If an error occurs here, cancel all the other goroutines
content, err := ReadFile(p)
if err != nil {
cancel(fmt.Errorf("error reading %s: %w", p, err))
log.Printf("ReadFilesConcurrently (goroutine): Context canceled for path %s: %v", p, err)
wg.Done()
return
}
resCh <- content
}
}(ctx, cancel, path, resCh, &wg)
}
go func() {
wg.Wait()
close(resCh)
}()
for c := range resCh {
contents = append(contents, c)
}
if e := ctx.Err(); e != nil {
return nil, fmt.Errorf("ReadFilesConcurrently: %w", e)
}
return contents, nil
}

View File

@ -0,0 +1,8 @@
package main
import "regexp"
func isValidPath(p string) bool {
pathRe := regexp.MustCompile(`(invalid regular expression`)
return pathRe.MatchString(p)
}

View File

@ -0,0 +1,5 @@
# Getting Started with The database/sql Package
This hands-on tutorial will show you how to get started with the database/sql package.
Find the tutorial [here](https://blog.jetbrains.com/go/2023/02/28/getting-started-with-the-database-sql-package/).

View File

@ -0,0 +1,43 @@
package main
import (
"database/sql"
"fmt"
"log"
"os"
"github.com/go-sql-driver/mysql"
)
var db *sql.DB
type Album struct {
ID int64
Title string
Artist string
Price float32
Quantity int64
}
func main() {
// Capture connection properties.
cfg := mysql.Config{
User: os.Getenv("DBUSER"),
Passwd: os.Getenv("DBPASS"),
Net: "tcp",
Addr: "127.0.0.1:3306",
DBName: "recordings",
}
// Get a database handle.
var err error
db, err = sql.Open("mysql", cfg.FormatDSN())
if err != nil {
log.Fatal(err)
}
pingErr := db.Ping()
if pingErr != nil {
log.Fatal(pingErr)
}
fmt.Println("Connected!")
}

View File

@ -0,0 +1,5 @@
module database-sql-package-goproject
go 1.19
require github.com/go-sql-driver/mysql v1.7.0 // indirect

View File

@ -0,0 +1,2 @@
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=

View File

@ -0,0 +1,68 @@
package main
import (
"database/sql"
"fmt"
"log"
"os"
"github.com/go-sql-driver/mysql"
)
var db *sql.DB
type Album struct {
ID int64
Title string
Artist string
Price float32
Quantity int64
}
func main() {
// Capture connection properties.
cfg := mysql.Config{
User: os.Getenv("DBUSER"),
Passwd: os.Getenv("DBPASS"),
Net: "tcp",
Addr: "127.0.0.1:3306",
DBName: "recordings",
}
// Get a database handle.
var err error
db, err = sql.Open("mysql", cfg.FormatDSN())
if err != nil {
log.Fatal(err)
}
pingErr := db.Ping()
if pingErr != nil {
log.Fatal(pingErr)
}
fmt.Println("Connected!")
albID, err := addAlbum(Album{
Title: "The Modern Sound of Betty Carter",
Artist: "Betty Carter",
Price: 49.99,
Quantity: 10,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("ID of added album: %v\n", albID)
}
// addAlbum adds the specified album to the database,
// returning the album ID of the new entry
func addAlbum(alb Album) (int64, error) {
result, err := db.Exec("INSERT INTO album (title, artist, price, quantity) VALUES (?, ?, ?, ?)", alb.Title, alb.Artist, alb.Price, alb.Quantity)
if err != nil {
return 0, fmt.Errorf("addAlbum: %v", err)
}
id, err := result.LastInsertId()
if err != nil {
return 0, fmt.Errorf("addAlbum: %v", err)
}
return id, nil
}

View File

@ -0,0 +1,73 @@
package main
import (
"database/sql"
"fmt"
"log"
"os"
"github.com/go-sql-driver/mysql"
)
var db *sql.DB
type Album struct {
ID int64
Title string
Artist string
Price float32
Quantity int64
}
func main() {
// Capture connection properties.
cfg := mysql.Config{
User: os.Getenv("DBUSER"),
Passwd: os.Getenv("DBPASS"),
Net: "tcp",
Addr: "127.0.0.1:3306",
DBName: "recordings",
}
// Get a database handle.
var err error
db, err = sql.Open("mysql", cfg.FormatDSN())
if err != nil {
log.Fatal(err)
}
pingErr := db.Ping()
if pingErr != nil {
log.Fatal(pingErr)
}
fmt.Println("Connected!")
albums, err := albumsByArtist("John Coltrane")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Albums found: %v\n", albums)
}
// albumsByArtist queries for albums that have the specified artist name.
func albumsByArtist(name string) ([]Album, error) {
// An albums slice to hold data from returned rows.
var albums []Album
rows, err := db.Query("SELECT * FROM album WHERE artist = ?", name)
if err != nil {
return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
}
defer rows.Close()
// Loop through rows, using Scan to assign column data to struct fields.
for rows.Next() {
var alb Album
if err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price, &alb.Quantity); err != nil {
return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
}
albums = append(albums, alb)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
}
return albums, nil
}

View File

@ -0,0 +1,73 @@
package main
import (
"database/sql"
"fmt"
"log"
"os"
"github.com/go-sql-driver/mysql"
)
var db *sql.DB
type Album struct {
ID int64
Title string
Artist string
Price float32
Quantity int64
}
func main() {
// Capture connection properties.
cfg := mysql.Config{
User: os.Getenv("DBUSER"),
Passwd: os.Getenv("DBPASS"),
Net: "tcp",
Addr: "127.0.0.1:3306",
DBName: "recordings",
}
// Get a database handle.
var err error
db, err = sql.Open("mysql", cfg.FormatDSN())
if err != nil {
log.Fatal(err)
}
pingErr := db.Ping()
if pingErr != nil {
log.Fatal(pingErr)
}
fmt.Println("Connected!")
// Hard-code ID 2 here to test the query.
Album, err := albumByID(2)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Album found: %v\n", Album)
}
// AlbumByID retrieves the specified album.
func albumByID(id int) (Album, error) {
// Define a prepared statement. You'd typically define the statement
// elsewhere and save it for use in functions such as this one.
stmt, err := db.Prepare("SELECT * FROM album WHERE id = ?")
if err != nil {
log.Fatal(err)
}
var album Album
// Execute the prepared statement, passing in an id value for the
// parameter whose placeholder is ?
err = stmt.QueryRow(id).Scan(&album.ID, &album.Title, &album.Artist, &album.Price, &album.Quantity)
if err != nil {
if err == sql.ErrNoRows {
// Handle the case of no rows returned.
}
return album, err
}
return album, nil
}

View File

@ -0,0 +1,65 @@
package main
import (
"database/sql"
"fmt"
"log"
"os"
"github.com/go-sql-driver/mysql"
)
var db *sql.DB
type Album struct {
ID int64
Title string
Artist string
Price float32
Quantity int64
}
func main() {
// Capture connection properties.
cfg := mysql.Config{
User: os.Getenv("DBUSER"),
Passwd: os.Getenv("DBPASS"),
Net: "tcp",
Addr: "127.0.0.1:3306",
DBName: "recordings",
}
// Get a database handle.
var err error
db, err = sql.Open("mysql", cfg.FormatDSN())
if err != nil {
log.Fatal(err)
}
pingErr := db.Ping()
if pingErr != nil {
log.Fatal(pingErr)
}
fmt.Println("Connected!")
// Hard-code ID 2 here to test the query.
alb, err := albumByID(2)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Album found: %v\n", alb)
}
// albumByID queries for the album with the specified ID.
func albumByID(id int64) (Album, error) {
// An album to hold data from the returned row.
var alb Album
row := db.QueryRow("SELECT * FROM album WHERE id = ?", id)
if err := row.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price, &alb.Quantity); err != nil {
if err == sql.ErrNoRows {
return alb, fmt.Errorf("albumsById %d: no such album", id)
}
return alb, fmt.Errorf("albumsById %d: %v", id, err)
}
return alb, nil
}

View File

@ -0,0 +1,17 @@
DROP TABLE IF EXISTS album;
CREATE TABLE album (
id INT AUTO_INCREMENT NOT NULL,
title VARCHAR(128) NOT NULL,
artist VARCHAR(255) NOT NULL,
price DECIMAL(5,2) NOT NULL,
quantity INT UNSIGNED,
PRIMARY KEY (`id`)
);
INSERT INTO album
(title, artist, price, quantity)
VALUES
('Blue Train', 'John Coltrane', 56.99, 5),
('Giant Steps', 'John Coltrane', 63.99, 62),
('Jeru', 'Gerry Mulligan', 17.99, 0),
('Sarah Vaughan', 'Sarah Vaughan', 34.98, 127);

View File

@ -0,0 +1,6 @@
DROP TABLE IF EXISTS album_trx;
CREATE TABLE album_trx (
trx_id INT AUTO_INCREMENT NOT NULL,
trx_check INT UNSIGNED,
PRIMARY KEY (`trx_id`)
);

View File

@ -0,0 +1,77 @@
package main
import (
"context"
"database/sql"
"fmt"
"log"
"os"
"github.com/go-sql-driver/mysql"
)
var db *sql.DB
type Album struct {
ID int64
Title string
Artist string
Price float32
Quantity int64
}
type Album_trx struct {
TRX_ID int64
TRX_CHECK int64
}
func main() {
// Capture connection properties.
cfg := mysql.Config{
User: os.Getenv("DBUSER"),
Passwd: os.Getenv("DBPASS"),
Net: "tcp",
Addr: "127.0.0.1:3306",
DBName: "recordings",
}
// Get a database handle.
var err error
db, err = sql.Open("mysql", cfg.FormatDSN())
if err != nil {
log.Fatal(err)
}
pingErr := db.Ping()
if pingErr != nil {
log.Fatal(pingErr)
}
fmt.Println("Connected!")
// Start the transaction
ctx := context.Background()
tx, err := db.BeginTx(ctx, nil)
if err != nil {
log.Fatal(err)
}
// First query
_, err = tx.ExecContext(ctx, "INSERT INTO album (title, artist, price, quantity) VALUES ('Master of Puppets', 'Metallica', '49', '1')")
if err != nil {
tx.Rollback()
return
}
// Second query
_, err = tx.ExecContext(ctx, "INSERT INTO album_trx (trx_check) VALUES (1)")
if err != nil {
tx.Rollback()
fmt.Println("Transaction declined")
return
}
// If no errors, commit the transaction
err = tx.Commit()
if err != nil {
log.Fatal(err)
}
fmt.Println("Transaction accepted!")
}

View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="RedisDemo 1: ping" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="get-started-with-redis" />
<working_directory value="$PROJECT_DIR$" />
<parameters value="ping" />
<kind value="PACKAGE" />
<package value="github.com/JetBrains/go-code-samples/get-started-with-redis" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" />
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="RedisDemo 2: get/set" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="get-started-with-redis" />
<working_directory value="$PROJECT_DIR$" />
<parameters value="getset" />
<kind value="PACKAGE" />
<package value="github.com/JetBrains/go-code-samples/get-started-with-redis" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" />
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="RedisDemo 3: expire" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="get-started-with-redis" />
<working_directory value="$PROJECT_DIR$" />
<parameters value="expire" />
<kind value="PACKAGE" />
<package value="github.com/JetBrains/go-code-samples/get-started-with-redis" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" />
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="RedisDemo 4: pipeline" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="get-started-with-redis" />
<working_directory value="$PROJECT_DIR$" />
<parameters value="pipeline" />
<kind value="PACKAGE" />
<package value="github.com/JetBrains/go-code-samples/get-started-with-redis" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" />
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="RedisDemo 5: transaction" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="get-started-with-redis" />
<working_directory value="$PROJECT_DIR$" />
<parameters value="transaction" />
<kind value="PACKAGE" />
<package value="github.com/JetBrains/go-code-samples/get-started-with-redis" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" />
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="RedisDemo 6: pub/sub" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="get-started-with-redis" />
<working_directory value="$PROJECT_DIR$" />
<parameters value="pubsub" />
<kind value="PACKAGE" />
<package value="github.com/JetBrains/go-code-samples/get-started-with-redis" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" />
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="RedisDemo: pipeline benchmark" type="GoTestRunConfiguration" factoryName="Go Test">
<module name="get-started-with-redis" />
<working_directory value="$PROJECT_DIR$" />
<kind value="DIRECTORY" />
<package value="github.com/JetBrains/go-code-samples/get-started-with-redis" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" />
<framework value="gobench" />
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="RedisDemo: reset data" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="get-started-with-redis" />
<working_directory value="$PROJECT_DIR$" />
<parameters value="reset" />
<kind value="PACKAGE" />
<package value="github.com/JetBrains/go-code-samples/get-started-with-redis" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$" />
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,5 @@
# Getting Started with Redis in Go
This tutorial demonstrates how to use Redis, the popular in-memory database that doubles as cache, pub/sub service, and streaming service, with Go and GoLand.
Find the tutorial [here]().

View File

@ -0,0 +1,35 @@
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
"time"
)
func expiringKeys(client *redis.Client) error {
ctx := context.Background()
// Add a temporary player
err := client.HSet(ctx, "player:10", "name", "Crymyios", "score", 0, "team", "Knucklewimp", "challenges_completed", 0).Err()
if err != nil {
return fmt.Errorf("cannot set player:10: %w", err)
}
// Set an expiration time for player:10
if !client.Expire(ctx, "player:10", time.Second).Val() {
return fmt.Errorf("cannot set expiration time for player:10")
}
// Get player:10
for i := 0; i < 3; i++ {
val, err := client.HGet(ctx, "player:10", "name").Result()
if err != nil {
fmt.Printf("player:10 has expired: %v\n", err)
return nil
}
fmt.Printf("player:10's name: %s\n", val)
time.Sleep(500 * time.Millisecond)
}
return nil
}

View File

@ -0,0 +1,32 @@
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// Task: The quest is a lowercase string. Change it to title case.
// Then print the challenges in the order they have to be completed.
func getAndSet(client *redis.Client) error {
ctx := context.Background()
quest, err := client.Get(ctx, "quest").Result()
if err != nil {
return fmt.Errorf("cannot get quest: %w", err)
}
quest = cases.Title(language.English).String(quest)
err = client.Set(ctx, "quest", quest, 0).Err()
if err != nil {
return fmt.Errorf("cannot update quest: %w", err)
}
fmt.Printf("Quest is now: %s\n", client.Get(ctx, "quest").Val())
return nil
}

View File

@ -0,0 +1,13 @@
module github.com/JetBrains/go-code-samples/get-started-with-redis
go 1.20
require (
github.com/redis/go-redis/v9 v9.0.5
golang.org/x/text v0.9.0
)
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
)

View File

@ -0,0 +1,18 @@
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o=
github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=

View File

@ -0,0 +1,103 @@
package main
import (
"fmt"
"log"
"os"
)
const (
dbconn = "localhost:6379"
db = 0
)
func usage() {
fmt.Println(`Usage: go run main.go <demo>
Where <demo> is one of:
- ping: run the ping command
- getset: get and set a string value
- expire: set an expiring key
- pipeline: run a batch of commands
- transaction: run a batch of commands that must all succeed
- pubsub: send a message to a channel and listen to the channel
- reset: restore the initial data set`)
}
func run() error {
// Create a new client from a connection string and a database number (0-15)
client := newClient(dbconn, 0)
if len(os.Args) < 2 {
usage()
return nil
}
switch os.Args[1] {
case "ping":
// Ping the redis server and fetch some database information.
fmt.Printf("\nPing: Test the connection\n")
ping(client)
case "getset":
// Get and set a string value.
fmt.Printf("\nGet/Set: Update the quest to title case\n")
err := getAndSet(client)
if err != nil {
return fmt.Errorf("getAndSet failed: %w", err)
}
case "expire":
// Set an expiring key and wait for it to expire
fmt.Printf("\nExpire: Add a player temporarily\n")
err := expiringKeys(client)
if err != nil {
return fmt.Errorf("expiringKeys failed: %w", err)
}
case "pipeline":
// Run a batch of commands.
fmt.Printf("\nPipeline: Update score and challenges_completed for team Snarkdumbthimble\n")
err := pipeline(client)
if err != nil {
return fmt.Errorf("pipeline failed: %w", err)
}
case "transaction":
// Run several commands that must all succeed.
// If any of them fails, the transaction will be canceled.
fmt.Printf("\nTransaction: Rearrange the teams\n")
err := transaction(client)
if err != nil {
return fmt.Errorf("transaction failed: %w", err)
}
case "pubsub":
// Send messages to a publish/receive channel.
// Listen to the channel and receive the messages.
fmt.Printf("\nPub/Sub: Publish challenges to subscribed teams\n")
err := pubsub(client)
if err != nil {
return fmt.Errorf("pubsub stopped: %w", err)
}
case "reset":
// Reset the database
fmt.Printf("\nReset the database\n")
err := resetdata(client)
if err != nil {
return fmt.Errorf("cannot reset database: %w", err)
}
default:
usage()
}
return nil
}
func main() {
err := run()
if err != nil {
log.Println(err)
os.Exit(1)
}
os.Exit(0)
}

View File

@ -0,0 +1,23 @@
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func ping(client *redis.Client) error {
// For the demo, we need only a background context
ctx := context.Background()
// Ping the redis server. It should respond with "PONG".
fmt.Println(client.Ping(ctx))
// Get the client info.
info, err := client.ClientInfo(ctx).Result()
if err != nil {
return fmt.Errorf("method ClientInfo failed: %w", err)
}
fmt.Printf("%#v\n", info)
return nil
}

View File

@ -0,0 +1,43 @@
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
// Task: Update the score and the challenges_completed
// for team Snarkdumbthimble that has finished challenge #1.
func pipeline(client *redis.Client) error {
ctx := context.Background()
_, err := client.Pipelined(ctx, func(pipe redis.Pipeliner) error {
err := pipe.HSet(ctx, "player:7", "score", 15, "challenges_completed", 1).Err()
if err != nil {
return err
}
err = pipe.HSet(ctx, "player:8", "score", 18, "challenges_completed", 1).Err()
if err != nil {
return err
}
err = pipe.HSet(ctx, "player:9", "score", 12, "challenges_completed", 1).Err()
return err
})
if err != nil {
return fmt.Errorf("pipelined failed: %w", err)
}
fmt.Printf("Player 7's score: %s, challenges completed: %s\n",
client.HGet(ctx, "player:7", "score").Val(),
client.HGet(ctx, "player:7", "challenges_completed").Val())
fmt.Printf("Player 8's score: %s, challenges completed: %s\n",
client.HGet(ctx, "player:8", "score").Val(),
client.HGet(ctx, "player:8", "challenges_completed").Val())
fmt.Printf("Player 9's score: %s, challenges completed: %s\n",
client.HGet(ctx, "player:9", "score").Val(),
client.HGet(ctx, "player:9", "challenges_completed").Val())
return nil
}

View File

@ -0,0 +1,53 @@
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
"testing"
)
func incrementScorePipe(client *redis.Client, player string) error {
ctx := context.Background()
client.Pipelined(ctx, func(pipe redis.Pipeliner) error {
for i := 0; i < 1000; i++ {
err := pipe.HIncrBy(ctx, player, "score", 1).Err()
if err != nil {
return fmt.Errorf("cannot increment score for player %s to %d: %w", player, i, err)
}
}
pipe.HSet(ctx, player, "score", 1)
return nil
})
return nil
}
func incrementScoreNoPipe(client *redis.Client, player string) error {
ctx := context.Background()
for i := 0; i < 1000; i++ {
err := client.HIncrBy(ctx, player, "score", 1).Err()
if err != nil {
return fmt.Errorf("cannot increment score for player %s to %d: %w", player, i, err)
}
}
client.HSet(ctx, player, "score", 1)
return nil
}
func BenchmarkPipeline(b *testing.B) {
client := newClient(dbconn, 0)
b.Run("PipelinedHIncrBy", func(b *testing.B) {
for i := 0; i < b.N; i++ {
incrementScorePipe(client, "player:1")
}
})
}
func BenchmarkNoPipeline(b *testing.B) {
client := newClient(dbconn, 0)
b.Run("HIncrBy", func(b *testing.B) {
for i := 0; i < b.N; i++ {
incrementScoreNoPipe(client, "player:2")
}
})
}

View File

@ -0,0 +1,164 @@
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
"time"
)
// Task: Send each challenge to the "challenge" channel.
// Each team is subscribed to the "challenge" channel and enters the challenge upon receiving.
const (
// the name of the PubSub channel
pubsubChan = "challenge"
)
// Res is the result of reading the pubsub channel
type Res struct {
result string
err error
}
// Team manages a team's subscription
// Each team uses its own client to subscribe to the "challenge" channel
type Team struct {
name string
client *redis.Client
channel *redis.PubSub
}
// getTeams scans the database for "team:*" keys
// and returns a slice of Team structs with names filled from the keys
func getTeams(client *redis.Client) []Team {
ctx := context.Background()
teams := make([]Team, 3)
teamsets := make([]string, 0, 3)
keys := make([]string, 0, 3)
var cursor uint64
for {
// Scan returns a slice of matches. The count may or may not be reached
// in the first call to Scan, so the code needs to call Scan in a loop and
// append the found keys to the teamsets slice until the cursor "returns to 0".
var err error
keys, cursor, err = client.Scan(ctx, cursor, "team:*", 3).Result()
if err != nil {
break
}
teamsets = append(teamsets, keys...)
if cursor == 0 {
break
}
}
// Lazily assume that the scan has returned 3 team sets
for i := 0; i < 3; i++ {
teams[i].name = teamsets[i]
// each team uses its own client
teams[i].client = newClient(dbconn, 0)
}
return teams
}
// subscribe subscribes to the "challenge" channel
// and waits for the subscription to be completed
func (team *Team) subscribe() error {
ctx := context.Background()
// Subscribe to the "challenge" channel
pubSub := team.client.Subscribe(ctx, pubsubChan)
// The first Subscribe() call creates the channel.
// Until that point, any attempt to publish something fails.
reply, err := pubSub.Receive(ctx)
if err != nil {
return fmt.Errorf("subscribing to channel '%s' failed: %w", pubsubChan, err)
}
// Expected response type is "*Subscription". Otherwise, something failed.
switch reply.(type) {
case *redis.Subscription:
// Success!
case *redis.Message:
// The channel is already active and contains messages, hence also a success
case *redis.Pong:
// letL's call it a success
default:
return fmt.Errorf("subscribing to a channel failed: received a reply of type %T, expected: *redis.Subscription", reply)
}
team.channel = pubSub
fmt.Printf("%s subscribed to channel '%s'\n", team.name, pubsubChan)
return nil
}
// receive receives messages from the "challenge" channel.
// It starts a goroutine that reads from the pubsub channel until
// the channel is closed or the context is done.
func (team *Team) receive(ctx context.Context, resChan chan<- Res) {
ch := team.channel.Channel()
defer close(resChan)
for {
select {
case msg, ok := <-ch:
if !ok {
// The pubsub channel has been closed
return
}
resChan <- Res{fmt.Sprintf("%s received challenge '%s'", team.name, msg.Payload), nil}
case <-ctx.Done():
resChan <- Res{"", ctx.Err()}
return
}
}
}
// publish publishes the challenge to the "challenge" channel
func publish(client *redis.Client, challenge string) error {
ctx := context.Background()
fmt.Printf("publishing challenge '%s'\n", challenge)
return client.Publish(ctx, pubsubChan, challenge).Err()
}
// pubsub subscribes to the "challenge" channel, publishes the challenges,
// and receives the published messages.
func pubsub(client *redis.Client) (err error) {
ctx := context.Background()
// Step 1: subscribe each team
teams := getTeams(client)
for i := 0; i < 3; i++ {
err = teams[i].subscribe()
if err != nil {
return fmt.Errorf("subscribing failed: %w", err)
}
}
// Step 2: publish challenges
// Read the challenges from the sorted set "challenges" and publish them
for i := int64(0); i < 5; i++ {
challenge := client.ZRange(ctx, "challenges", i, i).Val()[0]
err = publish(client, challenge)
if err != nil {
return fmt.Errorf("cannot publish challenge %s: %w", challenge, err)
}
}
// Close the channel after one second, to terminate the receive loops.
time.AfterFunc(time.Second, func() {
teams[0].channel.Close()
fmt.Println(`PubSub channel "challenges" closed`)
})
// Step 3: receive published messages
rch := make(chan Res)
for i := 0; i < 3; i++ {
go teams[i].receive(ctx, rch)
}
for msg := range rch {
if msg.err != nil {
return fmt.Errorf("cannot receive challenge: %w", msg.err)
}
fmt.Println(msg.result)
}
return nil
}

View File

@ -0,0 +1,11 @@
package main
import "github.com/redis/go-redis/v9"
func newClient(conn string, db int) *redis.Client {
return redis.NewClient(&redis.Options{
Addr: conn,
DB: db,
Password: "",
})
}

View File

@ -0,0 +1,57 @@
package main
import (
"context"
"encoding/csv"
"fmt"
"github.com/redis/go-redis/v9"
"io"
"os"
)
const (
setupfile = "testdata/setup.redis"
)
// resetdata flushes all data from the current database (db 0)
// and runs the commands from file setup.redis to set up the database.
func resetdata(client *redis.Client) error {
ctx := context.Background()
// read file "setup.redis" line by line
setup, err := os.Open(setupfile)
if err != nil {
return fmt.Errorf("cannot open %s: %w", setupfile, err)
}
defer setup.Close()
client.FlushDB(ctx) // FlushDB never fails.
csv := csv.NewReader(setup)
csv.Comma = ' '
csv.FieldsPerRecord = -1 // Variable number of fields per line
for {
cmd, err := csv.Read()
if err == io.EOF {
return nil
}
if err != nil {
return fmt.Errorf("csv: cannot read a line from %s: %w", setupfile, err)
}
// cmd is a slice of strings, Do() expects a slice of 'any'.
// The memory layout of the two slice types is not the same,
// so we need to convert cmd to a slice of 'any'.
doCmd := make([]interface{}, len(cmd))
for i, v := range cmd {
doCmd[i] = v
}
err = client.Do(ctx, doCmd...).Err()
if err != nil {
return fmt.Errorf("resetdata: cannot execute '%v': %w", cmd, err)
}
}
return nil
}

View File

@ -0,0 +1,16 @@
FLUSHDB
SET quest "the chicken coop catastrophe"
SET description "Players must rescue a group of chickens that have been taken hostage by a group of evil goblins."
HSET "player:1" name Sykios score 0 team Dorkfoot challenges_completed 0
HSET "player:2" name Nidios score 0 team Dorkfoot challenges_completed 0
HSET "player:3" name Tiaitia score 0 team Dorkfoot challenges_completed 0
HSET "player:4" name Belaeos score 0 team Knucklewimp challenges_completed 0
HSET "player:5" name Polytia score 0 team Knucklewimp challenges_completed 0
HSET "player:6" name Moritia score 0 team Knucklewimp challenges_completed 0
HSET "player:7" name Daryos score 0 team Snarkdumbthimble challenges_completed 0
HSET "player:8" name Blalios score 0 team Snarkdumbthimble challenges_completed 0
HSET "player:9" name Ighteatia score 0 team Snarkdumbthimble challenges_completed 0
SADD "team:Dorkfoot" Sykios Nidios Tiaitia
SADD "team:Knucklewimp" Belaeos Polytia Moritia
SADD "team:Snarkdumbthimble" Daryos Blalios Ighteatia
ZADD "challenges" 1 "Enter the hidden dungeon" 2 "Find the chicken coop" 3 "Defeat the goblins" 4 "Rescue the chickens" 5 "Escape the dungeon"

View File

@ -0,0 +1,76 @@
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
// Task:
// - Create a new team: Grumblebum
// - Move Sykios, Nidios, and Belaeos to the new team
// - Move Tiaitia to team Knucklewimp
// - Remove team Dorkfoot
func transaction(client *redis.Client) error {
ctx := context.Background()
_, err := client.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
// Move Sykios to team Grumblebum
err := pipe.HSet(ctx, "player:1", "team", "Grumblebum").Err()
if err != nil {
return err
}
// Move Nidios to team Grumblebum
err = pipe.HSet(ctx, "player:2", "team", "Grumblebum").Err()
if err != nil {
return err
}
// Move Belaeos to team Grumblebum
err = pipe.HSet(ctx, "player:4", "team", "Grumblebum").Err()
if err != nil {
return err
}
// Move Tiaitia to team Knucklewimp
err = pipe.HSet(ctx, "player:3", "team", "Knucklewimp").Err()
if err != nil {
return err
}
// Team update: remove Belaeos from team Knucklewimp
err = pipe.SRem(ctx, "team:Knucklewimp", "Belaeos").Err()
if err != nil {
return err
}
// Team update: add Tiaitia to team Knucklewimp
err = pipe.SAdd(ctx, "team:Knucklewimp", "Tiaitia").Err()
if err != nil {
return err
}
// Add team Grumblebum
err = pipe.SAdd(ctx, "team:Grumblebum", "Sykios", "Nidios", "Belaeos").Err()
if err != nil {
return err
}
// Remove team Dorkfoot. A set is removed by removing all elements.
err = pipe.SRem(ctx, "team:Dorkfoot", "Sykios", "Nidios", "Tiaitia").Err()
if err != nil {
return err
}
return nil
})
if err != nil {
return fmt.Errorf("TxPipelined failed: %w", err)
}
fmt.Printf("Sykios's new team: %s\n", client.HGet(ctx, "player:1", "team").Val())
fmt.Printf("Belaeos's new team: %s\n", client.HGet(ctx, "player:4", "team").Val())
fmt.Printf("Tiaitia's new team: %s\n", client.HGet(ctx, "player:3", "team").Val())
fmt.Printf("Team Grumblebum: %s\n", client.SMembers(ctx, "team:Grumblebum").Val())
fmt.Printf("Team Knucklewimp: %s\n", client.SMembers(ctx, "team:Knucklewimp").Val())
return nil
}

View File

@ -0,0 +1,5 @@
# Comparing database/sql, GORM, sqlx, and sqlc
This article compares the database/sql package with 3 other Go packages, namely: sqlx, sqlc, and GORM. The comparison focuses on 3 areas features, ease of use, and performance.
Find the article [here](https://blog.jetbrains.com/go/2023/04/27/comparing-db-packages/).

View File

@ -0,0 +1,127 @@
package benchmarks
import (
"context"
"database/sql"
"log"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
sqlc "github.com/rexfordnyrk/go-db-comparison/benchmarks/sqlc_generated"
)
func init() {
var err error
// Opening a database connection.
db, err = sql.Open("mysql", "theuser:thepass@tcp(localhost:3306)/thedb?multiStatements=true&parseTime=true")
if err != nil {
panic(err)
}
//sqlx connection using existing db connection
dbx = sqlx.NewDb(db, "mysql")
//sqlc connection using existing db connection
dbc = sqlc.New(db)
//gorm connection using existing db connection
gdb, err = gorm.Open(mysql.New(mysql.Config{Conn: db}))
if err != nil {
panic(err)
}
}
var (
gdb *gorm.DB
db *sql.DB
dbx *sqlx.DB
dbc *sqlc.Queries
)
func setup() {
clear()
table := `CREATE TABLE students (
id bigint NOT NULL AUTO_INCREMENT,
fname varchar(50) not null,
lname varchar(50) not null,
date_of_birth datetime not null,
email varchar(50) not null,
address varchar(50) not null,
gender varchar(50) not null,
PRIMARY KEY (id)
);`
_, err := db.Exec(table)
if err != nil {
panic(err)
}
//inserting records
_, err = db.Exec(records)
if err != nil {
panic(err)
}
}
func clear() {
_, err := db.Exec(`DROP TABLE IF EXISTS students;`)
if err != nil {
panic(err)
}
}
type Student struct {
ID int64
Fname string
Lname string
DateOfBirth time.Time `db:"date_of_birth"`
Email string
Address string
Gender string
}
func DbSqlQueryStudentWithLimit(limit int) {
var students []Student
rows, err := db.Query("SELECT * FROM students limit ?", limit)
if err != nil {
log.Fatalf("DbSqlQueryStudentWithLimit %d %v", limit, err)
}
defer rows.Close()
// Loop through rows, using Scan to assign column data to struct fields.
for rows.Next() {
var s Student
if err := rows.Scan(&s.ID, &s.Fname, &s.Lname, &s.DateOfBirth, &s.Email, &s.Address, &s.Gender); err != nil {
log.Fatalf("DbSqlQueryStudentWithLimit %d %v", limit, err)
}
students = append(students, s)
}
if err := rows.Err(); err != nil {
log.Fatalf("DbSqlQueryStudentWithLimit %d %v", limit, err)
}
}
func SqlxQueryStudentWithLimit(limit int) {
var students []Student
err := dbx.Select(&students, "SELECT * FROM students LIMIT ?", limit)
if err != nil {
log.Fatalf("SqlxQueryStudentWithLimit %d %v", limit, err)
}
}
func SqlcQueryStudentWithLimit(limit int) {
_, err := dbc.FetchStudents(context.Background(), int32(limit))
if err != nil {
log.Fatalf("SqlcQueryStudentWithLimit %d %v", limit, err)
}
}
func GormQueryStudentWithLimit(limit int) {
var students []Student
if err := gdb.Limit(limit).Find(&students).Error; err != nil {
log.Fatalf("GormQueryStudentWithLimit %d %v", limit, err)
}
}

View File

@ -0,0 +1,55 @@
package benchmarks
import (
"fmt"
"testing"
)
func Benchmark(b *testing.B) {
setup()
defer clear()
// Benchmark goes in here
limits := []int{
1,
10,
100,
1000,
10000,
15000,
}
for _, lim := range limits { // Fetch varying number of rows
fmt.Printf("================================== BENCHMARKING %d RECORDS ======================================\n", lim)
// Benchmark Database/sql
b.Run(fmt.Sprintf("Database/sql limit:%d ", lim), func(b *testing.B) {
for i := 0; i < b.N; i++ {
DbSqlQueryStudentWithLimit(lim)
}
})
// Benchmark Sqlx
b.Run(fmt.Sprintf("Sqlx limit:%d ", lim), func(b *testing.B) {
for i := 0; i < b.N; i++ {
SqlxQueryStudentWithLimit(lim)
}
})
// Benchmark Sqlc
b.Run(fmt.Sprintf("Sqlc limit:%d ", lim), func(b *testing.B) {
for i := 0; i < b.N; i++ {
SqlcQueryStudentWithLimit(lim)
}
})
// Benchmark GORM
b.Run(fmt.Sprintf("GORM limit:%d ", lim), func(b *testing.B) {
for i := 0; i < b.N; i++ {
GormQueryStudentWithLimit(lim)
}
})
fmt.Println("=================================================================================================")
}
}

View File

@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.16.0
package sqlc
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}

View File

@ -0,0 +1,19 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.16.0
package sqlc
import (
"time"
)
type Student struct {
ID int64
Fname string
Lname string
DateOfBirth time.Time
Email string
Address string
Gender string
}

View File

@ -0,0 +1,2 @@
-- name: FetchStudents :many
SELECT * FROM students LIMIT ?;

View File

@ -0,0 +1,45 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.16.0
// source: query.sql
package sqlc
import (
"context"
)
const fetchStudents = `-- name: FetchStudents :many
SELECT id, fname, lname, date_of_birth, email, address, gender FROM students LIMIT ?
`
func (q *Queries) FetchStudents(ctx context.Context, limit int32) ([]Student, error) {
rows, err := q.db.QueryContext(ctx, fetchStudents, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Student
for rows.Next() {
var i Student
if err := rows.Scan(
&i.ID,
&i.Fname,
&i.Lname,
&i.DateOfBirth,
&i.Email,
&i.Address,
&i.Gender,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@ -0,0 +1,10 @@
CREATE TABLE `students` (
`id` bigint NOT NULL AUTO_INCREMENT,
`fname` varchar(50) not null,
`lname` varchar(50) not null,
`date_of_birth` datetime not null,
`email` varchar(50) not null,
`address` varchar(50) not null,
`gender` varchar(50) not null,
PRIMARY KEY (`id`)
);

View File

@ -0,0 +1,7 @@
version: 1
packages:
- path: "./"
name: "sqlc"
engine: "mysql"
schema: "schema.sql"
queries: "query.sql"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,123 @@
package main
import (
"database/sql"
"fmt"
"log"
"time"
_ "github.com/go-sql-driver/mysql"
)
type Student struct {
ID int
Fname string
Lname string
DateOfBirth time.Time
Email string
Address string
Gender string
}
var db *sql.DB
func main() {
var err error
db, err = dbSqlConnect()
if err != nil {
log.Fatal(err)
}
s := Student{
Fname: "Leon",
Lname: "Ashling",
DateOfBirth: time.Date(1994, time.August, 14, 23, 51, 42, 0, time.UTC),
Email: "lashling5@senate.gov",
Address: "39 Kipling Pass",
Gender: "Male",
}
//adding student record to table
sID, err := addStudent(s)
if err != nil {
fmt.Println(err)
}
fmt.Printf("addSudent id: %v \n", sID)
//selecting student by ID
st, err := studentByID(sID)
if err != nil {
fmt.Println(err)
}
fmt.Printf("studentByID id: %v \n", st)
students, err := fetchStudents()
if err != nil {
fmt.Println(err)
}
fmt.Printf("fetchStudents count: %v \n", len(students))
}
func dbSqlConnect() (*sql.DB, error) {
// Opening a database connection.
db, err := sql.Open("mysql", "theuser:thepass@tcp(localhost:3306)/thedb?parseTime=true")
if err != nil {
return nil, err
}
fmt.Println("Connected!")
return db, nil
}
func addStudent(s Student) (int64, error) {
query := "insert into students (fname, lname, date_of_birth, email, gender, address) values (?, ?, ?, ?, ?, ?);"
result, err := db.Exec(query, s.Fname, s.Lname, s.DateOfBirth, s.Email, s.Gender, s.Address)
if err != nil {
return 0, fmt.Errorf("addStudent Error: %v", err)
}
id, err := result.LastInsertId()
if err != nil {
return 0, fmt.Errorf("addSudent Error: %v", err)
}
return id, nil
}
func fetchStudents() ([]Student, error) {
// A slice of Students to hold data from returned rows.
var students []Student
rows, err := db.Query("SELECT * FROM students")
if err != nil {
return nil, fmt.Errorf("fetchStudents %v", err)
}
defer rows.Close()
// Loop through rows, using Scan to assign column data to struct fields.
for rows.Next() {
var s Student
if err := rows.Scan(&s.ID, &s.Fname, &s.Lname, &s.DateOfBirth, &s.Email, &s.Address, &s.Gender); err != nil {
return nil, fmt.Errorf("fetchStudents %v", err)
}
students = append(students, s)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("fetchStudents %v", err)
}
return students, nil
}
func studentByID(id int64) (Student, error) {
var st Student
row := db.QueryRow("SELECT * FROM students WHERE id = ?", id)
if err := row.Scan(&st.ID, &st.Fname, &st.Lname, &st.DateOfBirth, &st.Email, &st.Address, &st.Gender); err != nil {
if err == sql.ErrNoRows {
return st, fmt.Errorf("studentById %d: no such student", id)
}
return st, fmt.Errorf("studentById %d: %v", id, err)
}
return st, nil
}

View File

@ -0,0 +1,48 @@
package main
import (
"fmt"
"log"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type Student struct {
ID int
Fname string
Lname string
DateOfBirth time.Time
Email string
Address string
Gender string
}
func main() {
// refer https://github.com/go-sql-driver/mysql#dsn-data-source-name for details
db, err := gorm.Open(mysql.Open("theuser:thepass@tcp(127.0.0.1:3306)/thedb?charset=utf8mb4&parseTime=True&loc=Local"))
if err != nil {
log.Fatal(err)
}
fmt.Println("Connected!")
//initializing record to be inserted
s := Student{
Fname: "Leon",
Lname: "Ashling",
DateOfBirth: time.Date(1994, time.August, 14, 23, 51, 42, 0, time.UTC),
Email: "lashling5@senate.gov",
Address: "39 Kipling Pass",
Gender: "Male",
}
//adds student record and returns the ID into the ID field
db.Create(&s)
fmt.Printf("addSudent id: %v \n", s.ID)
//selecting multiple record
var students []Student
db.Limit(10).Find(&students)
fmt.Printf("fetchStudents count: %v \n", len(students))
}

View File

@ -0,0 +1,22 @@
DROP TABLE IF EXISTS students;
CREATE TABLE `students` (
`id` bigint NOT NULL AUTO_INCREMENT,
`fname` varchar(50) not null,
`lname` varchar(50) not null,
`date_of_birth` datetime not null,
`email` varchar(50),
`address` varchar(50),
`gender` varchar(50),
PRIMARY KEY (`id`)
);
insert into students (id, fname, lname, date_of_birth, email, gender, address) values (1, 'Caddric', 'Likely', '2000-07-06 02:43:37', 'clikely0@wp.com', 'Male', '9173 Boyd Street');
insert into students (id, fname, lname, date_of_birth, email, gender, address) values (2, 'Jerad', 'Ciccotti', '1993-02-11 15:59:56', 'jciccotti1@bravesites.com', 'Male', '34 Declaration Drive');
insert into students (id, fname, lname, date_of_birth, email, gender, address) values (3, 'Hillier', 'Caslett', '1992-09-04 13:38:46', 'hcaslett2@hostgator.com', 'Male', '36 Duke Trail');
insert into students (id, fname, lname, date_of_birth, email, gender, address) values (4, 'Bertine', 'Roddan', '1991-02-18 09:10:05', 'broddan3@independent.co.uk', 'Female', '2896 Kropf Road');
insert into students (id, fname, lname, date_of_birth, email, gender, address) values (5, 'Theda', 'Brockton', '1991-10-29 09:08:48', 'tbrockton4@lycos.com', 'Female', '93 Hermina Plaza');
insert into students (id, fname, lname, date_of_birth, email, gender, address) values (6, 'Leon', 'Ashling', '1994-08-14 23:51:42', 'lashling5@senate.gov', 'Male', '39 Kipling Pass');
insert into students (id, fname, lname, date_of_birth, email, gender, address) values (7, 'Aldo', 'Pettitt', '1994-08-14 22:03:40', 'apettitt6@hexun.com', 'Male', '38 Dryden Road');
insert into students (id, fname, lname, date_of_birth, email, gender, address) values (8, 'Filmore', 'Cordingly', '1999-11-20 02:35:48', 'fcordingly7@163.com', 'Male', '34 Pawling Park');
insert into students (id, fname, lname, date_of_birth, email, gender, address) values (9, 'Katalin', 'MacCroary', '1994-11-08 11:59:19', 'kmaccroary8@cargocollective.com', 'Female', '2540 Maryland Parkway');
insert into students (id, fname, lname, date_of_birth, email, gender, address) values (10, 'Franky', 'Puddan', '1995-04-23 17:07:29', 'fpuddan9@psu.edu', 'Female', '3214 Washington Road');

View File

@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.17.2
package main
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}

View File

@ -0,0 +1,19 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.17.2
package main
import (
"time"
)
type Student struct {
ID int64
Fname string
Lname string
DateOfBirth time.Time
Email string
Address string
Gender string
}

View File

@ -0,0 +1,8 @@
-- name: addStudent :execlastid
insert into students (fname, lname, date_of_birth, email, gender, address) values (?, ?, ?, ?, ?, ?);
-- name: studentByID :one
SELECT * FROM students WHERE id = ?;
-- name: fetchStudents :many
SELECT * FROM students LIMIT 10;

View File

@ -0,0 +1,93 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.17.2
// source: query.sql
package main
import (
"context"
"time"
)
const addStudent = `-- name: addStudent :execlastid
insert into students (fname, lname, date_of_birth, email, gender, address) values (?, ?, ?, ?, ?, ?)
`
type addStudentParams struct {
Fname string
Lname string
DateOfBirth time.Time
Email string
Gender string
Address string
}
func (q *Queries) addStudent(ctx context.Context, arg addStudentParams) (int64, error) {
result, err := q.db.ExecContext(ctx, addStudent,
arg.Fname,
arg.Lname,
arg.DateOfBirth,
arg.Email,
arg.Gender,
arg.Address,
)
if err != nil {
return 0, err
}
return result.LastInsertId()
}
const fetchStudents = `-- name: fetchStudents :many
SELECT id, fname, lname, date_of_birth, email, address, gender FROM students LIMIT 10
`
func (q *Queries) fetchStudents(ctx context.Context) ([]Student, error) {
rows, err := q.db.QueryContext(ctx, fetchStudents)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Student
for rows.Next() {
var i Student
if err := rows.Scan(
&i.ID,
&i.Fname,
&i.Lname,
&i.DateOfBirth,
&i.Email,
&i.Address,
&i.Gender,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const studentByID = `-- name: studentByID :one
SELECT id, fname, lname, date_of_birth, email, address, gender FROM students WHERE id = ?
`
func (q *Queries) studentByID(ctx context.Context, id int64) (Student, error) {
row := q.db.QueryRowContext(ctx, studentByID, id)
var i Student
err := row.Scan(
&i.ID,
&i.Fname,
&i.Lname,
&i.DateOfBirth,
&i.Email,
&i.Address,
&i.Gender,
)
return i, err
}

View File

@ -0,0 +1,10 @@
CREATE TABLE `students` (
`id` bigint NOT NULL AUTO_INCREMENT,
`fname` varchar(50) not null,
`lname` varchar(50) not null,
`date_of_birth` datetime not null,
`email` varchar(50) not null,
`address` varchar(50) not null,
`gender` varchar(50) not null,
PRIMARY KEY (`id`)
);

View File

@ -0,0 +1,52 @@
package main
import (
"context"
"database/sql"
"fmt"
"log"
"time"
_ "github.com/go-sql-driver/mysql"
)
func main() {
conn, err := sql.Open("mysql", "theuser:thepass@tcp(localhost:3306)/thedb?parseTime=true")
if err != nil {
log.Fatal(err)
}
fmt.Println("Connected!")
db := New(conn)
//initializing record to be inserted
newSt := addStudentParams{
Fname: "Leon",
Lname: "Ashling",
DateOfBirth: time.Date(1994, time.August, 14, 23, 51, 42, 0, time.UTC),
Email: "lashling5@senate.gov",
Gender: "Male",
Address: "39 Kipling Pass",
}
// inserting the record
sID, err := db.addStudent(context.Background(), newSt)
if err != nil {
log.Fatal(err)
}
fmt.Printf("addSudent id: %v \n", sID)
//retreive record by id
st, err := db.studentByID(context.Background(), sID)
if err != nil {
log.Println(err)
}
fmt.Printf("studentByID record: %v \n", st)
//fetching multiple records
students, err := db.fetchStudents(context.Background())
if err != nil {
log.Println(err)
}
fmt.Printf("fetchStudents count: %v \n", len(students))
}

View File

@ -0,0 +1,7 @@
version: 1
packages:
- path: "./"
name: "main"
engine: "mysql"
schema: "schema.sql"
queries: "query.sql"

View File

@ -0,0 +1,114 @@
package main
import (
"database/sql"
"fmt"
"log"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
type Student struct {
ID int
Fname string
Lname string
DateOfBirth time.Time `db:"date_of_birth"`
Email string
Address string
Gender string
}
var db *sqlx.DB
func main() {
var err error
db, err = sqlxConnect()
if err != nil {
log.Fatal(err)
}
s := Student{
Fname: "Leon",
Lname: "Ashling",
DateOfBirth: time.Date(1994, time.August, 14, 23, 51, 42, 0, time.UTC),
Email: "lashling5@senate.gov",
Address: "39 Kipling Pass",
Gender: "Male",
}
//adding student record to table
sID, err := addStudent(s)
if err != nil {
fmt.Println(err)
}
fmt.Printf("addStudent id: %v \n", sID)
//selecting student by ID
st, err := studentByID(sID)
if err != nil {
fmt.Println(err)
}
fmt.Printf("studentByID record: %v \n", st)
students, err := fetchStudents()
if err != nil {
fmt.Println(err)
}
fmt.Printf("fetchStudents count: %v \n", len(students))
}
func sqlxConnect() (*sqlx.DB, error) {
// Opening a database connection.
db, err := sqlx.Open("mysql", "theuser:thepass@tcp(localhost:3306)/thedb?parseTime=true")
if err != nil {
return nil, err
}
fmt.Println("Connected!")
return db, nil
}
func addStudent(s Student) (int64, error) {
query := "insert into students (fname, lname, date_of_birth, email, gender, address) values (?, ?, ?, ?, ?, ?);"
result := db.MustExec(query, s.Fname, s.Lname, s.DateOfBirth, s.Email, s.Gender, s.Address)
id, err := result.LastInsertId()
if err != nil {
return 0, fmt.Errorf("addSudent Error: %v", err)
}
return id, nil
}
func fetchStudents() ([]Student, error) {
// A slice of Students to hold data from returned rows.
var students []Student
err := db.Select(&students, "SELECT * FROM students LIMIT 10")
if err != nil {
return nil, fmt.Errorf("fetchStudents %v", err)
}
return students, nil
}
func studentByID(id int64) (Student, error) {
var st Student
//if err := db.QueryRowx("SELECT * FROM students WHERE id = ?", id).StructScan(&st); err != nil {
// if err == sql.ErrNoRows {
// return st, fmt.Errorf("studentById %d: no such student", id)
// }
// return st, fmt.Errorf("studentById %d: %v", id, err)
//}
if err := db.Get(&st, "SELECT * FROM students WHERE id = ?", id); err != nil {
if err == sql.ErrNoRows {
return st, fmt.Errorf("studentById %d: no such student", id)
}
return st, fmt.Errorf("studentById %d: %v", id, err)
}
return st, nil
}

View File

@ -0,0 +1,10 @@
module github.com/rexfordnyrk/go-db-comparison
go 1.16
require (
github.com/go-sql-driver/mysql v1.6.0
github.com/jmoiron/sqlx v1.3.5
gorm.io/driver/mysql v1.4.4
gorm.io/gorm v1.24.6
)

View File

@ -0,0 +1,18 @@
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
gorm.io/driver/mysql v1.4.4 h1:MX0K9Qvy0Na4o7qSC/YI7XxqUw5KDw01umqgID+svdQ=
gorm.io/driver/mysql v1.4.4/go.mod h1:BCg8cKI+R0j/rZRQxeKis/forqRwRSYOR8OM3Wo6hOM=
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s=
gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=

View File

@ -0,0 +1,42 @@
module chat-app
go 1.20
require (
github.com/gin-gonic/gin v1.9.1
github.com/glebarez/go-sqlite v1.21.1
)
require (
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.3 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.21.1 // indirect
)

View File

@ -0,0 +1,103 @@
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/glebarez/go-sqlite v1.21.1 h1:7MZyUPh2XTrHS7xNEHQbrhfMZuPSzhkm2A1qgg0y5NY=
github.com/glebarez/go-sqlite v1.21.1/go.mod h1:ISs8MF6yk5cL4n/43rSOmVMGJJjHYr7L2MbZZ5Q4E2E=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.21.1 h1:GyDFqNnESLOhwwDRaHGdp2jKLDzpyT/rNLglX3ZkMSU=
modernc.org/sqlite v1.21.1/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -0,0 +1,271 @@
package main
import (
"database/sql"
"fmt"
"log"
"net/http"
"os"
"strconv"
"github.com/gin-gonic/gin"
_ "github.com/glebarez/go-sqlite"
)
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
}
type Channel struct {
ID int `json:"id"`
Name string `json:"name"`
}
type Message struct {
ID int `json:"id"`
ChannelID int `json:"channel_id"`
UserID int `json:"user_id"`
UserName string `json:"user_name"`
Text string `json:"text"`
}
func main() {
// Get the working directory
wd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
// Print the working directory
fmt.Println("Working directory:", wd)
// Open the SQLite database file
db, err := sql.Open("sqlite", wd+"/database.db")
defer func(db *sql.DB) {
err := db.Close()
if err != nil {
log.Fatal(err)
}
}(db)
// Create the Gin router
r := gin.Default()
if err != nil {
log.Fatal(err)
}
// Creation endpoints
r.POST("/users", func(c *gin.Context) { createUser(c, db) })
r.POST("/channels", func(c *gin.Context) { createChannel(c, db) })
r.POST("/messages", func(c *gin.Context) { createMessage(c, db) })
// Listing endpoints
r.GET("/channels", func(c *gin.Context) { listChannels(c, db) })
r.GET("/messages", func(c *gin.Context) { listMessages(c, db) })
// Login endpoint
r.POST("/login", func(c *gin.Context) { login(c, db) })
err = r.Run(":8080")
if err != nil {
log.Fatal(err)
}
}
// User creation endpoint
func createUser(c *gin.Context, db *sql.DB) {
// Parse JSON request body into User struct
var user User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Insert user into database
result, err := db.Exec("INSERT INTO users (username, password) VALUES (?, ?)", user.Username, user.Password)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Get ID of newly inserted user
id, err := result.LastInsertId()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Return ID of newly inserted user
c.JSON(http.StatusOK, gin.H{"id": id})
}
// Login endpoint
func login(c *gin.Context, db *sql.DB) {
// Parse JSON request body into User struct
var user User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Query database for user
row := db.QueryRow("SELECT id FROM users WHERE username = ? AND password = ?", user.Username, user.Password)
// Get ID of user
var id int
err := row.Scan(&id)
if err != nil {
// Check if user was not found
if err == sql.ErrNoRows {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid username or password"})
return
}
// Return error if other error occurred
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
// Return ID of user
c.JSON(http.StatusOK, gin.H{"id": id})
}
// Channel creation endpoint
func createChannel(c *gin.Context, db *sql.DB) {
// Parse JSON request body into Channel struct
var channel Channel
if err := c.ShouldBindJSON(&channel); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Insert channel into database
result, err := db.Exec("INSERT INTO channels (name) VALUES (?)", channel.Name)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Get ID of newly inserted channel
id, err := result.LastInsertId()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Return ID of newly inserted channel
c.JSON(http.StatusOK, gin.H{"id": id})
}
// Channel listing endpoint
func listChannels(c *gin.Context, db *sql.DB) {
// Query database for channels
rows, err := db.Query("SELECT id, name FROM channels")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Create slice of channels
var channels []Channel
// Iterate over rows
for rows.Next() {
// Create new channel
var channel Channel
// Scan row into channel
err := rows.Scan(&channel.ID, &channel.Name)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Append channel to slice
channels = append(channels, channel)
}
// Return slice of channels
c.JSON(http.StatusOK, channels)
}
// Message creation endpoint
func createMessage(c *gin.Context, db *sql.DB) {
// Parse JSON request body into Message struct
var message Message
if err := c.ShouldBindJSON(&message); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Insert message into database
result, err := db.Exec("INSERT INTO messages (channel_id, user_id, message) VALUES (?, ?, ?)", message.ChannelID, message.UserID, message.Text)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Get ID of newly inserted message
id, err := result.LastInsertId()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Return ID of newly inserted message
c.JSON(http.StatusOK, gin.H{"id": id})
}
// Message listing endpoint
func listMessages(c *gin.Context, db *sql.DB) {
// Parse channel ID from URL
channelID, err := strconv.Atoi(c.Query("channelID"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Parse optional limit query parameter from URL
limit, err := strconv.Atoi(c.Query("limit"))
if err != nil {
// Set limit to 100 if not provided
limit = 100
}
// Parse last message ID query parameter from URL. This is used to get messages after a certain message.
lastMessageID, err := strconv.Atoi(c.Query("lastMessageID"))
if err != nil {
// Set last message ID to 0 if not provided
lastMessageID = 0
}
// Query database for messages
rows, err := db.Query("SELECT m.id, channel_id, user_id, u.username AS user_name, message FROM messages m LEFT JOIN users u ON u.id = m.user_id WHERE channel_id = ? AND m.id > ? ORDER BY m.id ASC LIMIT ?", channelID, lastMessageID, limit)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Create slice of messages
var messages []Message
// Iterate over rows
for rows.Next() {
// Create new message
var message Message
// Scan row into message
err := rows.Scan(&message.ID, &message.ChannelID, &message.UserID, &message.UserName, &message.Text)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Append message to slice
messages = append(messages, message)
}
// Return slice of messages
c.JSON(http.StatusOK, messages)
}

View File

@ -0,0 +1,18 @@
CREATE TABLE users (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL,
password TEXT NOT NULL
);
CREATE TABLE channels (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE messages (
id INTEGER PRIMARY KEY,
channel_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
message TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

View File

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,41 @@
{
"name": "chat-ui",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"react-router-dom": "^6.13.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@latest/dist/tailwind.min.css" rel="stylesheet">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -0,0 +1,20 @@
import React from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import Login from './Login';
import CreateUser from './CreateUser';
import MainChat from './MainChat';
const App = () => {
return (
<Router>
<Routes>
<Route path="/create-user" element={<CreateUser />} />
<Route path="/chat" element={<MainChat />} />
<Route path="/chat/:channelId" element={<MainChat />} />
<Route path="/" element={<Login />} />
</Routes>
</Router>
);
};
export default App;

View File

@ -0,0 +1,79 @@
import React, { useState, useEffect } from 'react';
import {useParams} from "react-router-dom";
const ChannelsList = ({ selectedChannel, setSelectedChannel }) => {
const { channelId } = useParams();
const [channels, setChannels] = useState([]);
const [newChannelName, setNewChannelName] = useState('');
useEffect(() => {
if (channelId) {
const channel = channels.find((channel) => channel.id === parseInt(channelId));
if (channel) {
setSelectedChannel({name: channel.name, id: parseInt(channelId)});
}
}
}, [channelId, channels]);
useEffect(() => {
const fetchChannels = async () => {
const response = await fetch('/channels');
const data = await response.json();
setChannels(data || []);
};
fetchChannels();
}, []);
const handleAddChannel = async () => {
const response = await fetch('/channels', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newChannelName }),
});
if (response.ok) {
const newChannel = await response.json();
setChannels([...channels, { id: newChannel.id, name: newChannelName }]);
setNewChannelName('');
}
};
return (
<div className="flex flex-col h-full bg-gray-100 border-r">
<div className="bg-gray-700 text-white p-2">
Channels
</div>
<div className="overflow-y-auto flex-grow p-4">
{channels ? (
<ul className="w-full">
{channels.map((channel) => (
<li
key={channel.id}
className={`p-2 rounded-md w-full cursor-pointer ${parseInt(channelId) === channel.id ? 'bg-blue-500 text-white' : 'hover:bg-gray-200'}`}
onClick={() => setSelectedChannel(channel)}
>
{channel.name}
</li>
))}
</ul>
) : (
<div className="text-center text-gray-600">
Please add a Channel
</div>
)}
</div>
<div className="flex flex-col p-4">
<input
type="text"
value={newChannelName}
onChange={(e) => setNewChannelName(e.target.value)}
placeholder="New channel..."
className="mb-4 p-2 w-full border rounded-md bg-white"
/>
<button onClick={handleAddChannel} className="p-2 bg-blue-500 text-white rounded-md">Add Channel</button>
</div>
</div>
);
};
export default ChannelsList;

View File

@ -0,0 +1,56 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
const CreateUser = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
const response = await fetch('/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
if (response.ok) {
navigate('/');
} else {
alert('Account creation failed');
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<form onSubmit={handleSubmit} className="p-8 border rounded shadow-md">
<div className="mb-4">
<label htmlFor="username" className="block text-sm font-medium text-gray-700">Username</label>
<input
type="text"
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="mt-1 p-2 w-full border rounded-md"
required
/>
</div>
<div className="mb-4">
<label htmlFor="password" className="block text-sm font-medium text-gray-700">Password</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="mt-1 p-2 w-full border rounded-md"
required
/>
</div>
<button type="submit" className="w-full p-2 bg-blue-500 text-white rounded-md">Create Account</button>
</form>
</div>
);
};
export default CreateUser;

View File

@ -0,0 +1,63 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
const Login = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
const response = await fetch('/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
if (response.ok) {
const data = await response.json();
localStorage.setItem('userId', data.id);
localStorage.setItem('userName', username);
navigate('/chat');
} else {
alert('Login failed');
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<form onSubmit={handleSubmit} className="p-8 border rounded shadow-md">
<div className="mb-4">
<label htmlFor="username" className="block text-sm font-medium text-gray-700">Username</label>
<input
type="text"
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="mt-1 p-2 w-full border rounded-md"
required
/>
</div>
<div className="mb-4">
<label htmlFor="password" className="block text-sm font-medium text-gray-700">Password</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="mt-1 p-2 w-full border rounded-md"
required
/>
</div>
<button type="submit" className="w-full p-2 bg-blue-500 text-white rounded-md">Log In</button>
<div className="mt-4 text-center">
<span className="text-sm text-gray-600">Don't have an account? </span>
<a href="/create-user" className="text-blue-500 hover:underline">Create one</a>
</div>
</form>
</div>
);
};
export default Login;

View File

@ -0,0 +1,34 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import ChannelsList from './ChannelsList';
import MessagesPanel from './MessagesPanel';
const MainChat = () => {
const { channelId } = useParams();
const navigate = useNavigate();
const [selectedChannel, setSelectedChannel] = useState(parseInt(channelId) || null);
// If the component loads with a channel ID in the URL, set it as the selected channel.
useEffect(() => {
if (selectedChannel) {
navigate(`/chat/${selectedChannel.id}`);
}
}, [selectedChannel, navigate]);
const handleChannelSelect = (channelId) => {
setSelectedChannel(channelId);
};
return (
<div className="flex h-screen">
<div className="w-1/4 border-r">
<ChannelsList selectedChannel={selectedChannel} setSelectedChannel={handleChannelSelect} />
</div>
<div className="w-3/4">
<MessagesPanel selectedChannel={selectedChannel} />
</div>
</div>
);
};
export default MainChat;

View File

@ -0,0 +1,62 @@
import React, { useState } from 'react';
const MessageEntry = ({ selectedChannel, onNewMessage }) => {
const [text, setText] = useState('');
const handleSendMessage = async () => {
const userID = localStorage.getItem('userId');
const userName = localStorage.getItem('userName');
const response = await fetch('/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
"channel_id": parseInt(selectedChannel.id),
"user_id": parseInt(userID),
text
}),
});
if (response.ok) {
const message = await response.json();
onNewMessage({
id: message.id,
channel_id: selectedChannel,
user_id: userID,
user_name: userName,
text
});
setText('');
} else {
alert('Failed to send message');
}
};
const handleKeyDown = (event) => {
if (event.key === 'Enter' && !event.shiftKey) {
handleSendMessage();
event.preventDefault(); // Prevent the default behavior (newline)
}
};
return (
<div className="p-4 border-t flex">
<input
type="text"
placeholder="Type a message..."
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={handleKeyDown}
className="p-2 flex-grow border rounded-md mr-2"
/>
<button
onClick={handleSendMessage}
className="p-2 bg-blue-500 text-white rounded-md"
>
Send
</button>
</div>
);
};
export default MessageEntry;

View File

@ -0,0 +1,86 @@
import React, { useState, useEffect, useRef } from 'react';
import MessageEntry from './MessageEntry';
const MessagesPanel = ({ selectedChannel }) => {
const [messages, setMessages] = useState([]);
const lastMessageIdRef = useRef(null); // Keep track of the last message ID
useEffect(() => {
if (!selectedChannel) return;
let isMounted = true; // flag to prevent state updates after unmount
let intervalId = null;
const fetchMessages = async () => {
const response = await fetch(`/messages?channelID=${selectedChannel.id}`);
const data = await response.json();
if (isMounted) {
let messageData = data || [];
setMessages(messageData);
lastMessageIdRef.current = messageData.length > 0 ? messageData[messageData.length - 1].id : null;
}
};
fetchMessages();
intervalId = setInterval(() => {
if (lastMessageIdRef.current !== null) {
fetch(`/messages?channelID=${selectedChannel.id}&lastMessageID=${lastMessageIdRef.current}`)
.then(response => response.json())
.then(newMessages => {
if (isMounted && Array.isArray(newMessages) && newMessages.length > 0) {
setMessages((messages) => {
const updatedMessages = [...messages, ...newMessages];
lastMessageIdRef.current = updatedMessages[updatedMessages.length - 1].id;
return updatedMessages;
});
}
});
}
}, 5000); // Poll every 5 seconds
return () => {
isMounted = false; // prevent further state updates
clearInterval(intervalId); // clear interval on unmount
};
}, [selectedChannel]);
return (
<div className="flex flex-col h-full">
{selectedChannel && (
<div className="bg-gray-700 text-white p-2">
Messages for {selectedChannel.name}
</div>
)}
<div className={`overflow-auto flex-grow ${selectedChannel && messages.length === 0 ? 'flex items-center justify-center' : ''}`}>
{selectedChannel ? (
messages.length > 0 ? (
messages.map((message) => (
<div key={message.id} className="p-2 border-b">
<strong>{message.user_name}</strong>: {message.text}
</div>
))
) : (
<div className="text-center text-gray-600">
No messages yet! Why not send one?
</div>
)
) : (
<div className="p-2">Please select a channel</div>
)}
</div>
{selectedChannel && (
<MessageEntry
selectedChannel={selectedChannel}
onNewMessage={(message) => {
lastMessageIdRef.current = message.id;
setMessages([...messages, message])
}
}
/>
)}
</div>
);
};
export default MessagesPanel;

View File

@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

Some files were not shown because too many files have changed in this diff Show More